using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using static DSPRE.RomInfo;

namespace DSPRE.ROMFiles {
    /// <summary>
    /// Class to store script file data in Pokémon NDS games
    /// </summary>
    public class ScriptFile : RomFile {
        public List<ScriptCommandPosition> CommandSequence { get; set; } = new List<ScriptCommandPosition>();
        public Dictionary<int, string> OffsetToLabelMap { get; set; } = new Dictionary<int, string>();
        public enum ContainerTypes {
            Action,
            Script,
            Label
        };

        public struct ContainerReference {
            public uint ID;
            public uint offsetInFile;
        }

        public int fileID = -1;
        public bool isLevelScript = new bool();

        public bool HasNoScripts { get { return fileID == int.MaxValue; } }

        public static readonly char[] specialChars = { 'x', 'X', '#', '.', '_' };

        public ScriptFile(Stream fs) {
            // Initialize collections
            CommandSequence = new List<ScriptCommandPosition>();
            OffsetToLabelMap = new Dictionary<int, string>();

            using (BinaryReader br = new BinaryReader(fs)) {
                // Read header to find entry points
                List<int> entryPointOffsets = new List<int>();
                isLevelScript = true; // Is Level Script until proved otherwise

                try {
                    int entryPointIndex = 0;
                    while (true) {
                        long headerPos = br.BaseStream.Position;
                        uint checker = br.ReadUInt16();
                        br.BaseStream.Position -= 0x2;
                        uint value = br.ReadUInt32();

                        if (value == 0 && entryPointOffsets.Count == 0) {
                            isLevelScript = true;
                            break;
                        }

                        if (checker == 0xFD13) {
                            br.BaseStream.Position -= 0x4;
                            isLevelScript = false;
                            break;
                        }

                        int offsetFromStart = (int)(value + headerPos + 4);
                        entryPointOffsets.Add(offsetFromStart);

                        // Create entry point label
                        string entryLabel = $"script_{entryPointIndex}";
                        OffsetToLabelMap[offsetFromStart] = entryLabel;
                        entryPointIndex++;
                    }
                } catch (EndOfStreamException) {
                    if (!isLevelScript) {
                        MessageBox.Show("Script File couldn't be read correctly.", "Unexpected EOF", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }

                if (isLevelScript) {
                    return;
                }

                // Skip the 0xFD13 marker
                br.ReadUInt16();

                // Process commands until end of file
                while (br.BaseStream.Position < br.BaseStream.Length) {
                    int currentOffset = (int)br.BaseStream.Position;

                    // Sometimes we might hit padding bytes - try to detect and skip
                    if (br.BaseStream.Position + 2 <= br.BaseStream.Length) {
                        byte[] peekBytes = br.ReadBytes(2);
                        br.BaseStream.Position -= 2;

                        // Check if these look like padding
                        if (peekBytes[0] == 0 && peekBytes[1] == 0) {
                            // Skip padding byte
                            br.ReadByte();
                            continue;
                        }
                    }

                    ScriptCommand cmd = ReadCommand(br);
                    if (cmd == null || cmd.id == null) {
                        break; // End of file or error
                    }

                    // Check if this is an entry point or jump target
                    bool isEntryPoint = false;
                    int entryPointIndex = -1;
                    string label = null;

                    if (OffsetToLabelMap.TryGetValue(currentOffset, out string existingLabel)) {
                        label = existingLabel;
                        isEntryPoint = existingLabel.StartsWith("script_");
                        if (isEntryPoint) {
                            entryPointIndex = int.Parse(existingLabel.Substring("script_".Length));
                        }
                    }

                    // Add to command sequence
                    CommandSequence.Add(new ScriptCommandPosition(
                        cmd, currentOffset, label, isEntryPoint, entryPointIndex));
                }
            }
        }

        public ScriptFile(int fileID) : this(getFileStream(fileID)) {
            this.fileID = fileID;
        }

        public static FileStream getFileStream(int fileID) {
            string path = Filesystem.GetScriptPath(fileID);
            return new FileStream(path, FileMode.OpenOrCreate);
        }

        public override string ToString() {
            string prefix = isLevelScript ? "Level " : "";
            return $"{prefix}Script File " + this.fileID;
        }

        public ScriptFile(IEnumerable<string> lines, int fileID = -1) {
            CommandSequence = new List<ScriptCommandPosition>();
            OffsetToLabelMap = new Dictionary<int, string>();
            this.fileID = fileID;

            int currentOffset = 0;
            string currentLabel = null;
            int entryPointIndex = 0;
            bool isCurrentLabelEntryPoint = false;  // Track this instead

            // Parse each line
            foreach (var line in lines) {
                string trimmedLine = line.Trim();

                // Skip empty lines
                if (string.IsNullOrWhiteSpace(trimmedLine)) {
                    continue;
                }

                // Check if this is a label
                if (trimmedLine.EndsWith(":")) {
                    currentLabel = trimmedLine.Substring(0, trimmedLine.Length - 1);

                    // Track if it's an entry point
                    isCurrentLabelEntryPoint = currentLabel.StartsWith("script_");
                    if (isCurrentLabelEntryPoint) {
                        if (int.TryParse(currentLabel.Substring("script_".Length), out int index)) {
                            entryPointIndex = index;
                        }
                    }

                    continue;
                }

                // Parse the command
                ScriptCommand cmd = new ScriptCommand(trimmedLine);
                if (cmd.id == null) {
                    continue; // Skip invalid commands
                }

                // Calculate the size of this command for offset tracking
                int cmdSize = 2; // Command ID (2 bytes)
                foreach (var param in cmd.Parameters) {
                    cmdSize += param.RawData.Length;
                }

                // Use the tracked entry point flag
                int epIndex = isCurrentLabelEntryPoint ? entryPointIndex : -1;

                CommandSequence.Add(new ScriptCommandPosition(
                    cmd, currentOffset, currentLabel, isCurrentLabelEntryPoint, epIndex));

                // Map the offset to the label for jump targets
                if (currentLabel != null) {
                    OffsetToLabelMap[currentOffset] = currentLabel;
                    currentLabel = null; // Reset label
                    isCurrentLabelEntryPoint = false; // Reset entry point flag
                }

                // Update offset for next command
                currentOffset += cmdSize;
            }
        }

        private ScriptCommand ReadCommand(BinaryReader br) {
            // Check if we've reached the end of the file
            if (br.BaseStream.Position >= br.BaseStream.Length) {
                return null;
            }

            try {
                ushort id = br.ReadUInt16();
                List<ScriptParameter> parameters = new List<ScriptParameter>();

                // Track the original position for jump calculations
                long commandStartPos = br.BaseStream.Position - 2;

                switch (gameFamily) {
                    case GameFamilies.Plat:
                        switch (id) {
                            case 0x16: // Jump
                            case 0x1A: // Call
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x17: // JumpIfObjID
                            case 0x18: // JumpIfEventID
                            case 0x19: // JumpIfPlayerDir
                            case 0x1C: // JumpIf
                            case 0x1D: // CallIf
                                       // First parameter (condition)
                                parameters.Add(new ScriptParameter(new byte[] { br.ReadByte() }));
                                // Then jump target
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x5E: // Movement
                                parameters.Add(new ScriptParameter(BitConverter.GetBytes(br.ReadUInt16())));
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x1CF:
                            case 0x1D0:
                            case 0x1D1: {
                                    byte parameter1 = br.ReadByte();
                                    parameters.Add(new ScriptParameter(new byte[] { parameter1 }));
                                    if (parameter1 == 0x2) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(2))); //Read additional u16 if first param read is 2
                                    }
                                }
                                break;
                            case 0x21D: {
                                    ushort parameter1 = br.ReadUInt16();
                                    parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1)));

                                    switch (parameter1) {
                                        case 0:
                                        case 1:
                                        case 2:
                                        case 3:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 4:
                                        case 5:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 6:
                                            break;
                                    }
                                }
                                break;
                            case 0x235: {
                                    short parameter1 = br.ReadInt16();
                                    parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1)));

                                    switch (parameter1) {
                                        case 0x1:
                                        case 0x3:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x4:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x0:
                                        case 0x6:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        default:
                                            break;
                                    }
                                }
                                break;
                            case 0x23E: {
                                    short parameter1 = br.ReadInt16();
                                    parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1)));

                                    switch (parameter1) {
                                        case 0x1:
                                        case 0x3:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x5:
                                        case 0x6:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        default:
                                            break;
                                    }
                                }
                                break;
                            case 0x2C4: {
                                    byte parameter1 = br.ReadByte();
                                    parameters.Add(new ScriptParameter(new byte[] { parameter1 }));
                                    if (parameter1 == 0 || parameter1 == 1) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                    }
                                }
                                break;
                            case 0x2C5: {
                                    if (RomInfo.gameVersion == GameVersions.Platinum) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                        parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                    } else {
                                        goto default;
                                    }
                                }
                                break;
                            case 0x2C6:
                            case 0x2C9:
                            case 0x2CA:
                            case 0x2CD:
                                if (RomInfo.gameVersion == GameVersions.Platinum) {
                                    break;
                                } else {
                                    goto default;
                                }
                            case 0x2CF:
                                if (RomInfo.gameVersion == GameVersions.Platinum) {
                                    parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                    parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                } else {
                                    goto default;
                                }

                                break;
                            default:
                                // Standard command handling
                                if (RomInfo.ScriptCommandParametersDict.TryGetValue(id, out byte[] paramSizes)) {
                                    foreach (int size in paramSizes) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(size)));
                                    }
                                }
                                break;
                        }
                    break;

                    case GameFamilies.HGSS:
                        switch (id) {
                            case 0x16: //Jump
                            case 0x1A: //Call 
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x17: //JumpIfObjID
                            case 0x18: //JumpIfBgID
                            case 0x19: //JumpIfPlayerDir
                            case 0x1C: //JumpIf
                            case 0x1D: //CallIf
                                parameters.Add(new ScriptParameter(new byte[] { br.ReadByte() })); //in the case of JumpIf and CallIf, the first param is a comparisonOperator
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x5E: // Movement
                                parameters.Add(new ScriptParameter(BitConverter.GetBytes(br.ReadUInt16()))); //in the case of Movement, the first param is an overworld ID
                                ProcessRelativeJumpLinear(br, parameters);
                                break;

                            case 0x190:
                            case 0x191:
                            case 0x192: {
                                    byte parameter1 = br.ReadByte();
                                    parameters.Add(new ScriptParameter(new byte[] { parameter1 }));
                                    if (parameter1 == 0x2) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                    }
                                }
                                break;

                            case 0x1D1: // Number of parameters differ depending on the first parameter value
                            {
                                    short parameter1 = br.ReadInt16();
                                    parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1)));
                                    switch (parameter1) {
                                        case 0x0:
                                        case 0x1:
                                        case 0x2:
                                        case 0x3:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x4:
                                        case 0x5:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x6:
                                            break;
                                        case 0x7:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        default:
                                            break;
                                    }
                                }
                                break;

                            case 0x1E9: // Number of parameters differ depending on the first parameter value
                                {
                                    short parameter1 = br.ReadInt16();
                                    parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1)));
                                    switch (parameter1) {
                                        case 0x0:
                                            break;
                                        case 0x1:
                                        case 0x2:
                                        case 0x3:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x4:
                                            break;
                                        case 0x5:
                                        case 0x6:
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            parameters.Add(new ScriptParameter(br.ReadBytes(2)));
                                            break;
                                        case 0x7:
                                        case 0x8:
                                            break;
                                        default:
                                            break;
                                    }
                                }
                            break;

                            default:
                                // For standard commands, read parameters based on definition
                                if (RomInfo.ScriptCommandParametersDict.TryGetValue(id, out byte[] paramSizes)) {
                                    foreach (int size in paramSizes) {
                                        parameters.Add(new ScriptParameter(br.ReadBytes(size)));
                                    }
                                }
                                break;
                        }

                    break;
                }

                return new ScriptCommand(id, parameters);
            } catch (Exception ex) {
                Console.WriteLine($"Error reading command at offset {br.BaseStream.Position}: {ex.Message}");
                return null;
            }
        }

        private void ProcessRelativeJumpLinear(BinaryReader br, List<ScriptParameter> parameters) {
            // Read the relative offset
            int relativeOffset = br.ReadInt32();

            // Calculate absolute target
            int targetOffset = (int)(relativeOffset + br.BaseStream.Position);

            // Add to label map if not already there
            if (!OffsetToLabelMap.ContainsKey(targetOffset)) {
                string labelName = $"label_0x{targetOffset:X}";
                OffsetToLabelMap[targetOffset] = labelName;
            }

            // Create jump parameter
            string targetLabel = OffsetToLabelMap[targetOffset];
            ScriptParameter jumpParam = new ScriptParameter(relativeOffset, targetLabel) {
                Type = ScriptParameter.ParameterType.RelativeJump
            };
            parameters.Add(jumpParam);
        }

        // Convert to text - outputs commands in the order they appear in the binary
        public string ToText() {
            StringBuilder sb = new StringBuilder();

            foreach (var cmdPos in CommandSequence) {
                // If this command needs a label, output it
                if (!string.IsNullOrEmpty(cmdPos.Label)) {
                    sb.AppendLine($"\n{cmdPos.Label}:");
                }

                // Output the command (indented if not an entry point)
                string indent = "\t";
                sb.AppendLine($"{indent}{cmdPos.Command.name}");
            }

            return sb.ToString();
        }

        public override byte[] ToByteArray() {
            MemoryStream newData = new MemoryStream();
            using (BinaryWriter writer = new BinaryWriter(newData)) {
                // First, find all entry points and their positions in the command sequence
                var entryPoints = CommandSequence
                    .Where(c => c.IsEntryPoint)
                    .OrderBy(c => c.EntryPointIndex)
                    .ToList();

                // Allocate space for header
                long headerStart = writer.BaseStream.Position;
                writer.BaseStream.Position += entryPoints.Count * 4;
                writer.Write((ushort)0xFD13); // End of header marker

                // Keep track of command offsets in the new file
                Dictionary<int, long> oldOffsetToNewOffset = new Dictionary<int, long>();

                // Write all commands sequentially
                foreach (var cmdPos in CommandSequence) {
                    // Record the position of this command
                    oldOffsetToNewOffset[cmdPos.Offset] = writer.BaseStream.Position;

                    // Write the command
                    writer.Write((ushort)cmdPos.Command.id);

                    // Write parameters, handling jumps specially
                    foreach (var param in cmdPos.Command.Parameters) {
                        if (param.Type == ScriptParameter.ParameterType.RelativeJump) {
                            // For jump targets, we need to recalculate the relative offset
                            // based on the new positions of commands

                            // Find the target offset in the original file
                            int targetOffset = -1;
                            foreach (var kvp in OffsetToLabelMap) {
                                if (kvp.Value == param.TargetLabel) {
                                    targetOffset = kvp.Key;
                                    break;
                                }
                            }

                            if (targetOffset != -1 && oldOffsetToNewOffset.TryGetValue(targetOffset, out long newTargetOffset)) {
                                // Calculate new relative offset
                                int relativeOffset = (int)(newTargetOffset - (writer.BaseStream.Position + 4));
                                writer.Write(relativeOffset);
                            } else {
                                // Fallback - write original offset
                                writer.Write(param.TargetOffset);
                            }
                        } else {
                            // Regular parameter
                            writer.Write(param.RawData);
                        }
                    }
                }

                // Update header with entry point offsets
                writer.BaseStream.Position = headerStart;
                foreach (var entryPoint in entryPoints) {
                    if (oldOffsetToNewOffset.TryGetValue(entryPoint.Offset, out long newOffset)) {
                        uint relativeOffset = (uint)(newOffset - (headerStart + 4));
                        writer.Write(relativeOffset);
                    }
                }
            }

            return newData.ToArray();
        }

        public bool SaveToFileDefaultDir(int IDtoReplace, bool showSuccessMessage = true) {
            return SaveToFileDefaultDir(RomInfo.DirNames.scripts, IDtoReplace, showSuccessMessage);
        }

        public void SaveToFileExplorePath(string suggestedFileName, bool blindmode) {
            SaveFileDialog sf = new SaveFileDialog {
                Filter = "Gen IV Script File (*.scr)|*.scr"
            };

            if (!string.IsNullOrEmpty(suggestedFileName)) {
                sf.FileName = suggestedFileName;
            }

            if (sf.ShowDialog() != DialogResult.OK) {
                return;
            }

            if (blindmode) {
                string path = Filesystem.GetScriptPath(fileID);
                File.Copy(path, sf.FileName, overwrite: true);

                string msg = "";
                if (!isLevelScript) {
                    msg += "The last saved version of this ";
                }

                MessageBox.Show(msg + GetType().Name + " has been exported successfully.", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
            } else {
                this.SaveToFile(sf.FileName, showSuccessMessage: true);
            }
        }
    }
}
