using System.Diagnostics;
using System.Drawing.Imaging;
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using Core;
using Desktop.Audio;

namespace Desktop
{
    public partial class ParsonsForm1 : Form
    {
        public SmsMachine _machine = null!;
        private DebuggerForm _debugger;
        private VramViewerForm _vramViewer;
        private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb);
        private NAudioPlayer _audioPlayer;
        private Task _emulatorTask;
        public int TotalFrameCount = 0;
        public double FrameTime { get; private set; } = 0;
        public double FramesPerSecond { get; private set; } = 0;
        private string _currentRomName = "No ROM";
        private string _currentRomPath = "";
        private Stopwatch _stopwatch = new Stopwatch();
        private string _romType = "TBC";
        private bool isTurboMode = false;
        public bool IsRunning { get; private set; } = false;

        public ushort? Breakpoint
        {
            get => _machine?.Breakpoint;
            set { if (_machine != null) _machine.Breakpoint = value; }
        }

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
        public ParsonsForm1()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
        {
            InitializeComponent();
            this.KeyPreview = true;
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
            this.KeyDown += Form1_KeyDown;
#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
            this.KeyUp += Form1_KeyUp;
#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
            this.DoubleBuffered = true;
            this.ResizeRedraw = true;
        }
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}";

            this.BackColor = Color.Black;
            _machine = new SmsMachine();
            _audioPlayer = new NAudioPlayer();
            _machine.AudioProcessor.AudioDevice = _audioPlayer;
            IsRunning = false;

            PopulateIncludedRomsMenu();
        }

        private void DrawScreen(int[] safeFrame)
        {
            // Rapidly the Frame into the Windows Bitmap
            var data = _screenBitmap.LockBits(new Rectangle(0, 0, 256, 192), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

            //Frame buffer draw
            Marshal.Copy(safeFrame, 0, data.Scan0, safeFrame.Length);

            //Direct VDP draw
            //Marshal.Copy(_machine.VideoProcessor.FrameBuffer, 0, data.Scan0, _machine.VideoProcessor.FrameBuffer.Length);

            _screenBitmap.UnlockBits(data);
            this.Invalidate();
            TotalFrameCount++;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (_screenBitmap != null && menuStrip1 != null)
            {
                // 1. Maintain perfect, chunky retro pixels
                e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
                e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;

                int topOffset = menuStrip1.Height;
                int availableWidth = this.ClientSize.Width;
                int availableHeight = this.ClientSize.Height - topOffset;

                // 2. THE LENS LENS: Determine what part of the VDP buffer we actually want to show!
                // (Safely check if the machine/video processor exists yet)
                bool isGG = _machine?.VideoProcessor?.IsGameGear ?? false;

                int sourceWidth = isGG ? 160 : 256;
                int sourceHeight = isGG ? 144 : 192;
                int sourceX = isGG ? 48 : 0; // Skip 48 pixels in!
                int sourceY = isGG ? 24 : 0; // Skip 24 pixels down!

                // Create a rectangle defining the specific pixels to extract from the raw frame buffer
                Rectangle sourceRect = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight);

                // 3. Calculate the maximum scale factor for the cropped image
                float scaleX = (float)availableWidth / sourceWidth;
                float scaleY = (float)availableHeight / sourceHeight;

                // Pick the smaller scale so the image never bleeds off the edge
                float scale = Math.Min(scaleX, scaleY);

                // 4. Calculate the new physical pixel dimensions
                int newWidth = (int)(sourceWidth * scale);
                int newHeight = (int)(sourceHeight * scale);

                // 5. Center the image (Letterboxing / Pillarboxing)
                int offsetX = (availableWidth - newWidth) / 2;
                int offsetY = topOffset + ((availableHeight - newHeight) / 2);

                // Create a rectangle defining exactly where on the Windows form to draw the extracted pixels
                Rectangle destRect = new Rectangle(offsetX, offsetY, newWidth, newHeight);

                // 6. Blast the perfectly cropped and scaled bitmap onto the screen!
                e.Graphics.DrawImage(_screenBitmap, destRect, sourceRect, GraphicsUnit.Pixel);
            }
        }


        public void StartEmulator()
        {
            if (IsRunning) return;
            IsRunning = true;
            TotalFrameCount = 0;
            _emulatorTask = Task.Run(() =>
            {
                double targetFps = 59.94;
                double TargetFrameTime = 1000.0 / targetFps;
                _stopwatch.Restart();

                double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime;
                double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds;
                int framesThisSecond = 0;
                try
                {
                    while (IsRunning)
                    {
                        //targetFps = isTurboMode ? 1000 : 59.94;
                        //TargetFrameTime = 1000.0 / targetFps;
                        BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
                        {
                            targetFps = isTurboMode ? 1000 : 59.94;
                            TargetFrameTime = 1000.0 / targetFps;
                        });
                        double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds;

                        // --- POLL PHYSICAL CONTROLLER ---
                        if (XInput.XInputGetState(0, out XInput.XINPUT_STATE state) == 0)
                        {
                            ushort btns = state.Gamepad.wButtons;
                            byte padState = 0xFF;

                            if ((btns & 0x0001) != 0) padState &= 0xFE; // Up
                            if ((btns & 0x0002) != 0) padState &= 0xFD; // Down
                            if ((btns & 0x0004) != 0) padState &= 0xFB; // Left
                            if ((btns & 0x0008) != 0) padState &= 0xF7; // Right
                            if ((btns & 0x1000) != 0) padState &= 0xEF; // Button 1
                            if ((btns & 0x2000) != 0) padState &= 0xDF; // Button 2

                            // THE FIX: Constantly update the gamepad state, even when it's 0xFF!
                            _machine.IoBus.Joypad1Gamepad = padState;
                        }
                        // --------------------------------
                        _machine.RunFrame();

                        int[] frozenFrame = (int[])_machine.VideoProcessor.FrameBuffer.Clone();

                        // 2. FIRE AND FORGET! Tell Windows to draw, but DO NOT WAIT for it to finish!
                        BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(frozenFrame); });

                        // 3. Safety catch
                        if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value)
                        {
                            IsRunning = false;
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
                            BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); });
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
                            break;
                        }

                        // 4. Calculate TRUE Frame Time (Compute Time) BEFORE we go to sleep!
                        FrameTime = _stopwatch.Elapsed.TotalMilliseconds - frameStartTime;

                        // --- HIGH PRECISION THROTTLE ---
                        while (_stopwatch.Elapsed.TotalMilliseconds < nextFrameTargetTime)
                        {
                            if (nextFrameTargetTime - _stopwatch.Elapsed.TotalMilliseconds > 2.0)
                            {
                                Thread.Sleep(1);
                            }
                            else
                            {
                                Thread.SpinWait(10);
                            }
                        }

                        double currentTime = _stopwatch.Elapsed.TotalMilliseconds;
                        TotalFrameCount++;
                        framesThisSecond++;

                        if (currentTime - lastFpsUpdate >= 1000.0)
                        {
                            FramesPerSecond = framesThisSecond / ((currentTime - lastFpsUpdate) / 1000.0);
                            framesThisSecond = 0;
                            lastFpsUpdate = currentTime;

                            BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
                            {
                                this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName} [FPS: {FramesPerSecond:F0}]";
                            });
                        }

                        nextFrameTargetTime += TargetFrameTime;

                        if (currentTime > nextFrameTargetTime + TargetFrameTime)
                        {
                            nextFrameTargetTime = currentTime + TargetFrameTime;
                        }
                    }
                }
                catch (Exception ex)
                {
                    IsRunning = false;

                    this.Invoke((System.Windows.Forms.MethodInvoker)delegate
                    {
                        MessageBox.Show(
                            $"The Z80 CPU Panicked!\n\n" +
                            $"PC Address: 0x{_machine.Cpu.PC:X4}\n" +
                            $"Error: {ex.Message}\n\n" +
                            $"Stack Trace:\n{ex.StackTrace}",
                            "Fatal Emulator Crash",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Error);
                    });
                }
            });
        }

        public void StopEmulator()
        {
            IsRunning = false;
        }

        private async void LoadRomDataAndStart(byte[] romData, string fileName, string uniqueId)
        {
            StopEmulator();
            if (_emulatorTask != null)
            {
                await _emulatorTask;
            }

            SaveCurrentSram();

            // Blast the bytes straight into the virtual cartridge slot!
            _machine.LoadCartridge(romData);

            // Check the hardware mode based on the file name
            bool isGG = fileName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase);
            _machine.VideoProcessor.IsGameGear = isGG;

            // We use uniqueId to track the SRAM file path (works for both real paths and resource names)
            _currentRomPath = uniqueId;
            _currentRomName = Path.GetFileNameWithoutExtension(fileName);

            // Update the window title dynamically
            _romType = isGG ? "GG" : "MS";
            this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}";

            string savPath = GetSaveFilePath();
            if (savPath != null)
            {
                _machine.MemoryBus.LoadSaveData(savPath);
            }

            StartEmulator();
        }

        private void LoadRomAndStart(string filePath)
        {
            if (!File.Exists(filePath)) return;

            // Read from the hard drive, then pass to the universal loader
            byte[] romData = File.ReadAllBytes(filePath);
            LoadRomDataAndStart(romData, Path.GetFileName(filePath), filePath);
        }

        
        private string GetSaveFilePath()
        {
            // Don't try to save if a game hasn't been loaded yet!
            if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM")
#pragma warning disable CS8603 // Possible null reference return.
                return null;
#pragma warning restore CS8603 // Possible null reference return.

            // Application.StartupPath is the exact folder containing your .exe
            string exeFolder = Application.StartupPath;

            // Combines the exe folder with "GameName.sav"
            return Path.Combine(exeFolder, _currentRomName + ".sav");
        }
        private void SaveCurrentSram()
        {
            if (_machine != null)
            {
                string savPath = GetSaveFilePath();
                if (savPath != null)
                {
                    _machine.MemoryBus.SaveSaveData(savPath);
                }
            }
        }

        private void PopulateIncludedRomsMenu()
        {
            string romsDirectory = "Desktop.ROMS";
            string[] files = GetFilesInResourceDirectory(romsDirectory);

            foreach (string resourceName in files)
            {
                // 1. Strip the "Desktop.ROMS." prefix
                string cleanFileName = resourceName.Replace("Desktop.ROMS.", "");

                // 2. Strip the extension for a beautiful menu item (e.g. "Sonic")
                string menuName = Path.GetFileNameWithoutExtension(cleanFileName);

                ToolStripMenuItem romMenuItem = new ToolStripMenuItem(menuName);

                // 3. Point the click event to our new extraction helper!
                romMenuItem.Click += (sender, e) => LoadEmbeddedRom(resourceName);

                // 4. Sort into the correct menu based on extension!
                if (resourceName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase))
                {
                    gameGearToolStripMenuItem.DropDownItems.Add(romMenuItem);
                }
                else if (resourceName.EndsWith(".sms", StringComparison.OrdinalIgnoreCase))
                {
                    masterSystemToolStripMenuItem.DropDownItems.Add(romMenuItem);
                }
            }
        }

        private void LoadEmbeddedRom(string resourceName)
        {
            // 1. Strip off the ugly prefix to get the real file name for the UI and the .gg check
            string cleanFileName = resourceName.Replace("Desktop.ROMS.", "");

            Assembly assembly = Assembly.GetExecutingAssembly();

            // 2. Open a direct pipe to the internal resource
            using (Stream stream = assembly.GetManifestResourceStream(resourceName))
            {
                if (stream == null) return;

                // 3. Copy the stream entirely in RAM—never touching the hard drive!
                using (MemoryStream ms = new MemoryStream())
                {
                    stream.CopyTo(ms);
                    byte[] romData = ms.ToArray();

                    // 4. Pass the raw bytes to our universal loader
                    LoadRomDataAndStart(romData, cleanFileName, resourceName);
                }
            }
        }

        public static string[] GetFilesInResourceDirectory(string directoryPrefix)
        {
            // Get the assembly containing the embedded resources. 
            // Change this if your resources are in a different project/assembly.
            Assembly assembly = Assembly.GetExecutingAssembly();

            // Ensure the prefix ends with a dot so we don't accidentally match 
            // similarly named folders (e.g., matching "Folder" but not "FolderTwo")
            if (!directoryPrefix.EndsWith("."))
            {
                directoryPrefix += ".";
            }

            // Get all resource names and filter by our "directory" prefix
            string[] allResources = assembly.GetManifestResourceNames();

            string[] filesInDirectory = allResources
                .Where(resource => resource.StartsWith(directoryPrefix, StringComparison.OrdinalIgnoreCase))
                .ToArray();

            return filesInDirectory;
        }

        private void selectROMToolStripMenuItem_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog ofd = new OpenFileDialog())
            {
                ofd.Title = "Select Master System ROM";
                ofd.Filter = "SMS ROMs (*.sms)|*.sms|GG ROMs (*.gg)|*.gg|All Files (*.*)|*.*";

                if (ofd.ShowDialog() == DialogResult.OK)
                {
                    LoadRomAndStart(ofd.FileName);
                    _machine.Reset();
                }
            }
        }

        private void resetToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _machine.Reset();
        }

        private void debuggerToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_debugger == null || _debugger.IsDisposed)
            {
                _debugger = new DebuggerForm(_machine.Cpu, _machine.MemoryBus, this);
            }
            _debugger.Show();
        }

        private void vramViewerToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_vramViewer == null || _vramViewer.IsDisposed)
            {
                // Pass the VDP directly to the viewer!
                _vramViewer = new VramViewerForm(_machine.VideoProcessor);
            }
            _vramViewer.Show();
        }


        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            UpdateJoypad(e.KeyCode, true);
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            UpdateJoypad(e.KeyCode, false);
        }

        private void UpdateJoypad(Keys key, bool isPressed)
        {
            if (_machine == null) return;

            byte bitMask = 0;

            // Map your keys to the Sega hardware bits
            switch (key)
            {
                case Keys.W: bitMask = 0x01; break; // Bit 0: Up
                case Keys.S: bitMask = 0x02; break; // Bit 1: Down
                case Keys.A: bitMask = 0x04; break; // Bit 2: Left
                case Keys.D: bitMask = 0x08; break; // Bit 3: Right
                case Keys.O: bitMask = 0x10; break; // Bit 4: Button 1 (Start/Action)
                case Keys.P: bitMask = 0x20; break; // Bit 5: Button 2
                default: return; // Ignore any other keys
            }

            if (isPressed)
            {
                _machine.IoBus.Joypad1Keyboard &= (byte)~bitMask; // Target Keyboard!
            }
            else
            {
                _machine.IoBus.Joypad1Keyboard |= bitMask;        // Target Keyboard!
            }
        }

        private void ParsonsForm1_FormClosing(object sender, FormClosingEventArgs e)
        {
            IsRunning = false;
            SaveCurrentSram();
            _audioPlayer?.Stop();
        }
        private async void saveStateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return;

            // 1. Politely ask the emulator loop to pause, and wait for it to finish its current frame!
            StopEmulator();
            if (_emulatorTask != null) await _emulatorTask;

            // 2. Change the extension to .state and save it right next to the .exe
            string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state");
            _machine.SaveState(statePath);

            // 3. Resume the emulator seamlessly
            StartEmulator();
        }

        private async void loadStateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return;

            string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state");
            if (!File.Exists(statePath)) return;

            // 1. Pause the emulator
            StopEmulator();
            if (_emulatorTask != null) await _emulatorTask;

            // 2. Inject the frozen state into the silicon!
            _machine.LoadState(statePath);

            // 3. Resume
            StartEmulator();
        }

        private void turboModeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            turboModeToolStripMenuItem.Checked = !turboModeToolStripMenuItem.Checked;
            isTurboMode = turboModeToolStripMenuItem.Checked ? true : false;
        }

        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {
            MessageBox.Show(
                            $"First full release of Parsons Master System Emulator 2026!\r\n\r\nFeatures (v1.0):\r\n\r\n" +
                            $"    *Full compatibility with all Master System and Game Gear ROMs\r\n" +
                            $"    *Full window scaling with fixed aspect ratio (SMS & GG)\r\n" +
                            $"    *Full 4 channel SN76489 implememtation using NAudio\r\n" +
                            $"    *SRAM auto save and load (Battery Backed RAM)\r\n" +
                            $"    *Keyboard and XInput control pad support\r\n" +
                            $"    *Comprehensive Debugger\r\n" +
                            $"    *VRAM Viewer\r\n" +
                            $"    *Single point Save State\r\n" +
                            $"    *Turbo mode\r\n" +
                            $"    *Includes a selection of commercial ROMs\r\n\r\n" +
                            $"Planned updates (v1.1):\r\n\r\n" +
                            $"    *Architectural double buffering (prevent screen tear)\r\n" +
                            $"    *Graphics shaders (scanlines etc...)\r\n" +
                            $"    *Add multiple Save States per game\r\n\r\n\r\n" +
                            $"(c) Marc Parsons 2026","Parsons Master System 2026 v1.0",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Information);
        }
    }
}
