/*
 * Copyright 2019 faddenSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;

using Asm65;
using CommonUtil;
using SourceGen.Sandbox;

namespace SourceGen {
    /// <summary>
    /// All state for an open project.
    /// 
    /// This class does no file I/O or user interaction.
    /// </summary>
    public class DisasmProject {
        // Arbitrary 1MB limit.  Could be increased to 16MB if performance is acceptable.
        public const int MAX_DATA_FILE_SIZE = 1 << 20;

        // File magic.
        private const long MAGIC = 6982516645493599905;


        #region Data that is saved to the project file
        // All data held by structures in this section are persistent, and will be
        // written to the project file.  Anything not in this section may be discarded
        // at any time.  Smaller items are kept in arrays, with one entry per byte
        // of file data.

        /// <summary>
        /// Length of input data.  (This is redundant with FileData.Length while in memory,
        /// but we want this value to be serialized into the project file.)
        /// </summary>
        public int FileDataLength { get; private set; }

        /// <summary>
        /// CRC-32 on input data.
        /// </summary>
        public uint FileDataCrc32 { get; private set; }

        /// <summary>
        /// Map file offsets to addresses.
        /// </summary>
        public AddressMap AddrMap { get; private set; }

        /// <summary>
        /// Type hints.  Default value is "no hint".
        /// </summary>
        public CodeAnalysis.TypeHint[] TypeHints { get; private set; }

        /// <summary>
        /// Status flag overrides.  Default value is "all unspecified".
        /// </summary>
        public StatusFlags[] StatusFlagOverrides { get; private set; }

        /// <summary>
        /// End-of-line comments.  Empty string means "no comment".
        /// </summary>
        public string[] Comments { get; private set; }

        /// <summary>
        /// Full line, possibly multi-line comments.
        /// </summary>
        public Dictionary<int, MultiLineComment> LongComments { get; private set; }

        /// <summary>
        /// Notes, which are like comments but not included in the assembled output.
        /// </summary>
        public SortedList<int, MultiLineComment> Notes { get; private set; }

        /// <summary>
        /// Labels, defined by the user; uses file offset as key.  Ideally the label names
        /// are unique, but there are ways around that.
        /// </summary>
        public Dictionary<int, Symbol> UserLabels { get; private set; }

        /// <summary>
        /// Local variable tables.  Uses file offset as key.
        /// </summary>
        public SortedList<int, LocalVariableTable> LvTables { get; private set; }

        /// <summary>
        /// Format descriptors for operands and data items.  Uses file offset as key.
        /// </summary>
        public SortedList<int, FormatDescriptor> OperandFormats { get; private set; }

        /// <summary>
        /// Visualization sets.  Uses file offset as key.
        /// </summary>
        /// <remarks>
        /// TODO(maybe): certain operations must be performed on the set of all visualizations
        /// or the set of all animations, requiring a double "foreach" with an "is" clause.
        /// It might be simpler and more efficient overall to split this into three lists: one
        /// for visualizations (perhaps keyed by serial number), one for animations, and one
        /// for VisualizationSet objects (the latter required to establish ordering).  The
        /// primary argument against is that this makes undo/redo more complicated.
        /// </remarks>
        public SortedList<int, VisualizationSet> VisualizationSets { get; private set; }

        /// <summary>
        /// Project properties.  Includes CPU type, platform symbol file names, project
        /// symbols, etc.
        /// </summary>
        public ProjectProperties ProjectProps { get; private set; }

        #endregion // data to save & restore


        /// <summary>
        /// The contents of the 65xx data file.
        /// </summary>
        public byte[] FileData { get { return mFileData; } }
        private byte[] mFileData;

        /// <summary>
        /// CPU definition to use when analyzing input.
        /// </summary>
        public CpuDef CpuDef { get; private set; }

        /// <summary>
        /// If set, changes are ignored.
        /// </summary>
        public bool IsReadOnly { get; set; }

        /// <summary>
        /// If true, plugins will execute in the main application's AppDomain instead of
        /// the sandbox.  Must be set before calling Initialize().
        /// </summary>
        public bool UseMainAppDomainForPlugins { get; set; }

        /// <summary>
        /// Full pathname of project file.  The directory name is needed when loading
        /// platform symbols and extension scripts from the project directory, and the
        /// filename is used to give project-local extension scripts unique DLL names.
        ///
        /// For a new project that hasn't been saved yet, this will be empty.
        /// </summary>
        public string ProjectPathName { get; set; }

        // Filename only of data file.  This is used for debugging and text export.
        public string DataFileName { get; private set; }

        // This holds working state for the code and data analyzers.  Some of the state
        // is presented directly to the user, e.g. status flags.  All of the data here
        // should be considered transient; it may be discarded at any time without
        // causing user data loss.
        private Anattrib[] mAnattribs;

        // A snapshot of the Anattribs array, taken after code analysis has completed,
        // before data analysis has begun.
        private Anattrib[] mCodeOnlyAnattribs;

        // Symbol lists loaded from platform symbol files.  This is essentially a list
        // of lists, of symbols.
        private List<PlatformSymbols> PlatformSyms { get; set; }

        // Extension script manager.  Controls AppDomain sandbox.
        private ScriptManager mScriptManager;

        // All symbols, including user-defined, platform-specific, and auto-generated, keyed by
        // label string.  This is rebuilt whenever we do a refresh, and modified whenever
        // labels or platform definitions are edited.
        //
        // Note this includes project/platform symbols that will not be in the assembled output.
        public SymbolTable SymbolTable { get; private set; }

        // Cross-reference data, indexed by file offset.
        private Dictionary<int, XrefSet> mXrefs = new Dictionary<int, XrefSet>();

        // Project and platform symbols that are being referenced from code.
        public List<DefSymbol> ActiveDefSymbolList { get; private set; }

        // List of messages, mostly problems detected during analysis.
        public MessageList Messages { get; private set; }

        public class CodeDataCounts {
            public int CodeByteCount { get; set; }
            public int DataByteCount { get; set; }
            public int JunkByteCount { get; set; }

            public void Reset() {
                CodeByteCount = DataByteCount = JunkByteCount = 0;
            }
        }
        public CodeDataCounts ByteCounts { get; private set; } = new CodeDataCounts();

#if DATA_PRESCAN
        // Data scan results.
        public TypedRangeSet RepeatedBytes { get; private set; }
        public RangeSet StdAsciiBytes { get; private set; }
        public RangeSet HighAsciiBytes { get; private set; }
#endif

        // List of changes for undo/redo.
        private List<ChangeSet> mUndoList = new List<ChangeSet>();

        // Index of slot where next undo operation will be placed.
        private int mUndoTop = 0;

        // Index of top when the file was last saved.
        private int mUndoSaveIndex = 0;


        /// <summary>
        /// Constructs a new project.
        /// </summary>
        public DisasmProject() { }

        /// <summary>
        /// Prepares the object by instantiating various fields, some of which are sized to
        /// match the length of the data file.  The data file may not have been loaded yet
        /// (e.g. when deserializing a project file).
        /// </summary>
        public void Initialize(int fileDataLen) {
            Debug.Assert(FileDataLength == 0);      // i.e. Initialize() hasn't run yet
            Debug.Assert(fileDataLen > 0);

            FileDataLength = fileDataLen;
            ProjectPathName = string.Empty;

            AddrMap = new AddressMap(fileDataLen);
            AddrMap.Set(0, 0x1000);    // default load address to $1000; override later

            // Default value is "no hint".
            TypeHints = new CodeAnalysis.TypeHint[fileDataLen];

            // Default value is "unspecified" for all bits.
            StatusFlagOverrides = new StatusFlags[fileDataLen];

            Comments = new string[fileDataLen];

            // Populate with empty strings so we don't have to worry about null refs.
            for (int i = 0; i < Comments.Length; i++) {
                Comments[i] = string.Empty;
            }

            LongComments = new Dictionary<int, MultiLineComment>();
            Notes = new SortedList<int, MultiLineComment>();

            UserLabels = new Dictionary<int, Symbol>();
            OperandFormats = new SortedList<int, FormatDescriptor>();
            LvTables = new SortedList<int, LocalVariableTable>();
            VisualizationSets = new SortedList<int, VisualizationSet>();
            ProjectProps = new ProjectProperties();

            SymbolTable = new SymbolTable();
            PlatformSyms = new List<PlatformSymbols>();
            ActiveDefSymbolList = new List<DefSymbol>();

            Messages = new MessageList();

            // Default to 65816.  This will be replaced with value from project file or
            // system definition.
            ProjectProps.CpuType = CpuDef.CpuType.Cpu65816;
            ProjectProps.IncludeUndocumentedInstr = false;
            ProjectProps.TwoByteBrk = false;
            UpdateCpuDef();
        }

        /// <summary>
        /// Discards resources, notably the sandbox AppDomain.
        /// </summary>
        public void Cleanup() {
            Debug.WriteLine("DisasmProject.Cleanup(): scriptMgr=" + mScriptManager);
            if (mScriptManager != null) {
                mScriptManager.Cleanup();
                mScriptManager = null;
            }
        }

        /// <summary>
        /// Prepares the DisasmProject for use as a new project.
        /// </summary>
        /// <param name="fileData">65xx data file contents.</param>
        /// <param name="dataFileName">Data file's filename (not pathname).</param>
        public void PrepForNew(byte[] fileData, string dataFileName) {
            Debug.Assert(fileData.Length == FileDataLength);

            mFileData = fileData;
            DataFileName = dataFileName;
            FileDataCrc32 = CommonUtil.CRC32.OnWholeBuffer(0, mFileData);
#if DATA_PRESCAN
            ScanFileData();
#endif

            // Mark the first byte as code so we have something to do.  This may get
            // overridden later.
            TypeHints[0] = CodeAnalysis.TypeHint.Code;
        }

        /// <summary>
        /// Pulls items of interest out of the system definition object and applies them
        /// to the project.  Call this after LoadDataFile() for a new project.
        /// </summary>
        /// <param name="sysDef">Target system definition.</param>
        public void ApplySystemDef(SystemDef sysDef) {
            CpuDef.CpuType cpuType = CpuDef.GetCpuTypeFromName(sysDef.Cpu);
            bool includeUndoc = SystemDefaults.GetUndocumentedOpcodes(sysDef);
            bool twoByteBrk = SystemDefaults.GetTwoByteBrk(sysDef);
            CpuDef tmpDef = CpuDef.GetBestMatch(cpuType, includeUndoc, twoByteBrk);

            // Store the best-matched CPU in properties, rather than whichever was originally
            // requested.  This way the behavior of the project is the same for everyone, even
            // if somebody has a newer app version with specialized handling for the
            // originally-specified CPU.
            ProjectProps.CpuType = tmpDef.Type;
            ProjectProps.IncludeUndocumentedInstr = includeUndoc;
            ProjectProps.TwoByteBrk = twoByteBrk;
            UpdateCpuDef();

            ProjectProps.AnalysisParams.DefaultTextScanMode =
                SystemDefaults.GetTextScanMode(sysDef);

            ProjectProps.EntryFlags = SystemDefaults.GetEntryFlags(sysDef);

            // Configure the load address.
            if (SystemDefaults.GetFirstWordIsLoadAddr(sysDef) && mFileData.Length > 2) {
                // First two bytes are the load address, code starts at offset +000002.  We
                // need to put the load address into the stream, but don't want it to get
                // picked up as an address for something else.  So we set it to the same
                // address as the start of the file.  The overlapping-address code should do
                // the right thing with it.
                //
                // Updated after adding the "load address" report to the address edit dialog.
                // We don't want the two-byte offset.
                int loadAddr = RawData.GetWord(mFileData, 0, 2, false);
                AddrMap.Set(0, loadAddr - 2);
                AddrMap.Set(2, loadAddr);
                OperandFormats[0] = FormatDescriptor.Create(2, FormatDescriptor.Type.NumericLE,
                    FormatDescriptor.SubType.None);
                TypeHints[0] = CodeAnalysis.TypeHint.NoHint;
                TypeHints[2] = CodeAnalysis.TypeHint.Code;
            } else {
                int loadAddr = SystemDefaults.GetLoadAddress(sysDef);
                AddrMap.Set(0, loadAddr);
            }

            foreach (string str in sysDef.SymbolFiles) {
                ProjectProps.PlatformSymbolFileIdentifiers.Add(str);
            }
            foreach (string str in sysDef.ExtensionScripts) {
                ProjectProps.ExtensionScriptFileIdentifiers.Add(str);
            }
        }

        public void UpdateCpuDef() {
            CpuDef = CpuDef.GetBestMatch(ProjectProps.CpuType,
                ProjectProps.IncludeUndocumentedInstr, ProjectProps.TwoByteBrk);
        }

        /// <summary>
        /// Sets the file CRC.  Called during deserialization.
        /// </summary>
        /// <param name="crc">Data file CRC.</param>
        public void SetFileCrc(uint crc) {
            Debug.Assert(FileDataLength > 0);
            FileDataCrc32 = crc;
        }

        /// <summary>
        /// Sets the file data array.  Used when the project is created from a project file.
        /// </summary>
        /// <param name="fileData">65xx data file contents.</param>
        /// <param name="dataFileName">Data file's filename (not pathname).</param>
        /// <param name="report">Reporting object for validation errors.</param>
        public void SetFileData(byte[] fileData, string dataFileName, ref FileLoadReport report) {
            Debug.Assert(fileData.Length == FileDataLength);
            Debug.Assert(CRC32.OnWholeBuffer(0, fileData) == FileDataCrc32);
            mFileData = fileData;
            DataFileName = dataFileName;

            FixAndValidate(ref report);

#if DATA_PRESCAN
            ScanFileData();
#endif
        }

#if DATA_PRESCAN
        private delegate bool ByteTest(byte val);   // for ScanFileData()

        /// <summary>
        /// Scans the contents of the file data array, noting runs of identical bytes and
        /// other interesting bits.
        /// 
        /// The file data is guaranteed not to change, so doing a bit of work here can save
        /// us time during data analysis.
        /// </summary>
        private void ScanFileData() {
            DateTime startWhen = DateTime.Now;
            // Find runs of identical bytes.
            TypedRangeSet repeats = new TypedRangeSet();

            Debug.Assert(mFileData.Length > 0);
            byte matchByte = mFileData[0];
            int count = 1;
            for (int i = 1; i < mFileData.Length; i++) {
                if (mFileData[i] == matchByte) {
                    count++;
                    continue;
                }
                if (count >= DataAnalysis.MIN_RUN_LENGTH) {
                    repeats.AddRange(i - count, i - 1, matchByte);
                }
                matchByte = mFileData[i];
                count = 1;
            }
            if (count >= DataAnalysis.MIN_RUN_LENGTH) {
                repeats.AddRange(mFileData.Length - count, mFileData.Length - 1, matchByte);
            }

            RangeSet ascii = new RangeSet();
            CreateByteRangeSet(ascii, mFileData, DataAnalysis.MIN_STRING_LENGTH,
                delegate (byte val) {
                    return val >= 0x20 && val < 0x7f;
                }
            );
            RangeSet highAscii = new RangeSet();
            CreateByteRangeSet(highAscii, mFileData, DataAnalysis.MIN_STRING_LENGTH,
                delegate (byte val) {
                    return val >= 0xa0 && val < 0xff;
                }
            );

            if (false) {
                repeats.DebugDump("Repeated-Bytes (" + DataAnalysis.MIN_RUN_LENGTH + "+)");
                ascii.DebugDump("Standard-ASCII (" + DataAnalysis.MIN_STRING_LENGTH + "+)");
                highAscii.DebugDump("High-ASCII (" + DataAnalysis.MIN_STRING_LENGTH + "+)");
            }
            Debug.WriteLine("ScanFileData took " +
                ((DateTime.Now - startWhen).TotalMilliseconds) + " ms");

            RepeatedBytes = repeats;
            StdAsciiBytes = ascii;
            HighAsciiBytes = highAscii;
        }

        private void CreateByteRangeSet(RangeSet set, byte[] data, int minLen, ByteTest tester) {
            int count = 0;
            for (int i = 0; i < data.Length; i++) {
                if (tester(data[i])) {
                    count++;
                } else if (count < minLen) {
                    count = 0;
                } else {
                    set.AddRange(i - count, i - 1);
                    count = 0;
                }
            }
            if (count >= minLen) {
                set.AddRange(data.Length - count, data.Length - 1);
            }
        }
#endif

        /// <summary>
        /// Walks the list of format descriptors, fixing places where the data doesn't match.
        /// This is run once, after the file is loaded.
        /// </summary>
        private void FixAndValidate(ref FileLoadReport report) {
            // Can't modify a list while we're iterating through it, so gather changes here.
            Dictionary<int, FormatDescriptor> changes = new Dictionary<int, FormatDescriptor>();

            foreach (KeyValuePair<int, FormatDescriptor> kvp in OperandFormats) {
                FormatDescriptor dfd = kvp.Value;

                // v1 project files specified string layouts as sub-types, and assumed they
                // were high or low ASCII.  Numeric values could use the ASCII sub-type, which
                // included both high and low.
                //
                // v2 project files changed this to make string layouts types, with the
                // character encoding specified in the sub-type.  High and low ASCII became
                // separate, explicitly specified items.
                //
                // When loading a v1 file, the old "Ascii" sub-type is deserialized to
                // ASCII_GENERIC.  Now that we have access to the file data, we need to refine
                // the sub-type to high or low.
                if (dfd.FormatSubType == FormatDescriptor.SubType.ASCII_GENERIC) {
                    FormatDescriptor newDfd;
                    if (dfd.IsString) {
                        // Determine the string encoding by looking at the first character.
                        // For some strings (StringL8, StringL16) we need to skip forward a
                        // byte or two.  Empty strings with lengths or null-termination will
                        // be treated as low ASCII.
                        int checkOffset = kvp.Key;
                        if (dfd.FormatType == FormatDescriptor.Type.StringL8 && dfd.Length > 1) {
                            checkOffset++;
                        } else if (dfd.FormatType == FormatDescriptor.Type.StringL16 && dfd.Length > 2) {
                            checkOffset += 2;
                        }
                        bool isHigh = (FileData[checkOffset] & 0x80) != 0;
                        newDfd = FormatDescriptor.Create(dfd.Length, dfd.FormatType,
                            isHigh ? FormatDescriptor.SubType.HighAscii :
                                FormatDescriptor.SubType.Ascii);
                    } else if (dfd.IsNumeric) {
                        // This is a character constant in an instruction or data operand, such
                        // as ".dd1 'f'" or "LDA #'f'".  Could be multi-byte (even instructions
                        // can be 16-bit).  This is a little awkward, because at this point we
                        // can't tell the difference between instructions and data.
                        //
                        // However, we do know that instructions are always little-endian, that
                        // opcodes are one byte, that data values > $ff can't be ASCII encoded,
                        // and that $00 isn't a valid ASCII character.  So we can apply the
                        // following test:
                        // - if the length is 1, it's data; grab the first byte
                        // - if it's NumericBE, it's data; grab the last byte
                        // - if the second byte is $00, it's data; grab the first byte
                        // - otherwise, it's an instruction; grab the second byte
                        int checkOffset;
                        if (dfd.FormatType == FormatDescriptor.Type.NumericBE) {
                            Debug.Assert(dfd.Length <= FormatDescriptor.MAX_NUMERIC_LEN);
                            checkOffset = kvp.Key + dfd.Length - 1;
                        } else if (dfd.Length < 2 || FileData[kvp.Key + 1] == 0x00) {
                            checkOffset = kvp.Key;
                        } else {
                            Debug.Assert(dfd.FormatType == FormatDescriptor.Type.NumericLE);
                            checkOffset = kvp.Key + 1;
                        }
                        bool isHigh = (FileData[checkOffset] & 0x80) != 0;
                        newDfd = FormatDescriptor.Create(dfd.Length, dfd.FormatType,
                            isHigh ? FormatDescriptor.SubType.HighAscii :
                                FormatDescriptor.SubType.Ascii);
                    } else {
                        Debug.Assert(false);
                        newDfd = dfd;
                    }
                    changes[kvp.Key] = newDfd;
                    Debug.WriteLine("Fix +" + kvp.Key.ToString("x6") + ": " +
                        dfd + " -> " + newDfd);
                    // possibly interesting, but rarely; very noisy
                    //report.Add(FileLoadItem.Type.Notice,
                    //    "Fixed format at +" + kvp.Key.ToString("x6"));
                }
            }

            // Run through the list again, this time looking for badly-formed strings.  We're
            // only checking structure, not character encoding, because you're allowed to have
            // non-printable characters in strings.
            foreach (KeyValuePair<int, FormatDescriptor> kvp in OperandFormats) {
                FormatDescriptor dfd = kvp.Value;
                if (dfd.IsString && !DataAnalysis.VerifyStringData(FileData, kvp.Key, dfd.Length,
                        dfd.FormatType, out string failMsg)) {
                    report.Add(FileLoadItem.Type.Warning,
                        "+" + kvp.Key.ToString("x6") + ": " + failMsg);
                    changes[kvp.Key] = null;
                }
            }

            // Apply changes to main list.
            foreach (KeyValuePair<int, FormatDescriptor> kvp in changes) {
                if (kvp.Value == null) {
                    OperandFormats.Remove(kvp.Key);
                } else {
                    OperandFormats[kvp.Key] = kvp.Value;
                }
            }
        }

        /// <summary>
        /// Loads platform symbol files and extension scripts.
        /// 
        /// Call this on initial load and whenever the set of platform symbol files changes
        /// in the project config.
        /// 
        /// Failures here will be reported to the user but aren't fatal.
        /// </summary>
        /// <returns>Multi-line string with all warnings from load process.</returns>
        public string LoadExternalFiles() {
            TaskTimer timer = new TaskTimer();
            timer.StartTask("Total");

            StringBuilder sb = new StringBuilder();

            string projectDir = string.Empty;
            if (!string.IsNullOrEmpty(ProjectPathName)) {
                projectDir = Path.GetDirectoryName(ProjectPathName);
            }

            // Load the platform symbols first.
            timer.StartTask("Platform Symbols");
            PlatformSyms.Clear();
            int loadOrdinal = 0;
            foreach (string fileIdent in ProjectProps.PlatformSymbolFileIdentifiers) {
                PlatformSymbols ps = new PlatformSymbols();
                bool ok = ps.LoadFromFile(fileIdent, projectDir, loadOrdinal,
                    out FileLoadReport report);
                if (ok) {
                    PlatformSyms.Add(ps);
                }
                if (report.Count > 0) {
                    sb.Append(report.Format());
                }
                loadOrdinal++;
            }
            timer.EndTask("Platform Symbols");

            // Instantiate the script manager on first use.
            timer.StartTask("Create ScriptManager");
            if (mScriptManager == null) {
                mScriptManager = new ScriptManager(this);
            } else {
                mScriptManager.Clear();
            }
            timer.EndTask("Create ScriptManager");

            // Load the extension script files.
            timer.StartTask("Load Extension Scripts");
            foreach (string fileIdent in ProjectProps.ExtensionScriptFileIdentifiers) {
                bool ok = mScriptManager.LoadPlugin(fileIdent, out FileLoadReport report);
                if (report.Count > 0) {
                    sb.Append(report.Format());
                }
            }
            timer.EndTask("Load Extension Scripts");

            timer.EndTask("Total");
            timer.DumpTimes("Time to load external files:");

            return sb.ToString();
        }

        /// <summary>
        /// Checks some stuff.  Problems are handled with assertions, so this is only
        /// useful in debug builds.
        /// </summary>
        public void Validate() {
            // Confirm that we can walk through the file, stepping directly from the start
            // of one thing to the start of the next.  We won't normally do this, because
            // we need to watch for embedded instructions.
            int offset = 0;
            while (offset < mFileData.Length) {
                Anattrib attr = mAnattribs[offset];
                bool thisIsCode = attr.IsInstructionStart;
                Debug.Assert(attr.IsStart);
                Debug.Assert(attr.Length != 0);
                offset += attr.Length;

                // Sometimes embedded instructions continue past the "outer" instruction,
                // usually because we're misinterpreting the code.  We need to deal with
                // that here.
                //
                // One fun way to cause this is to have inline data from a plugin that got
                // overwritten by the code analyzer.  See test 2022 for an example.
                int extraInstrBytes = 0;
                while (offset < mFileData.Length && mAnattribs[offset].IsInstruction &&
                        !mAnattribs[offset].IsInstructionStart) {
                    extraInstrBytes++;
                    offset++;
                }

                // Make sure the extra code bytes were part of an instruction.  Otherwise it
                // means we moved from the end of a data area to the middle of an instruction,
                // which is very bad.
                Debug.Assert(extraInstrBytes == 0 || thisIsCode);

                //if (extraInstrBytes > 0) { Debug.WriteLine("EIB=" + extraInstrBytes); }
                // Max instruction len is 4, so the stray part must be shorter.
                Debug.Assert(extraInstrBytes < 4);
            }
            Debug.Assert(offset == mFileData.Length);

            // Confirm that all bytes are tagged as code, data, or inline data.  The Asserts
            // in Anattrib should confirm that nothing is tagged as more than one thing.
            for (offset = 0; offset < mAnattribs.Length; offset++) {
                Anattrib attr = mAnattribs[offset];
                Debug.Assert(attr.IsInstruction || attr.IsInlineData || attr.IsData);
            }

            // Confirm that there are no Default format entries in OperandFormats.
            foreach (KeyValuePair<int, FormatDescriptor> kvp in OperandFormats) {
                Debug.Assert(kvp.Value.FormatType != FormatDescriptor.Type.Default);
                Debug.Assert(kvp.Value.FormatType != FormatDescriptor.Type.REMOVE);
            }
        }

        /// <summary>
        /// Checks to see if any part of the address map runs across a bank boundary.
        /// </summary>
        private void ValidateAddressMap() {
            foreach (AddressMap.AddressMapEntry entry in AddrMap) {
                if ((entry.Addr & 0xff0000) != ((entry.Addr + entry.Length - 1) & 0xff0000)) {
                    string fmt = Res.Strings.MSG_BANK_OVERRUN_DETAIL_FMT;
                    int firstNext = (entry.Addr & 0xff0000) + 0x010000;
                    int badOffset = entry.Offset + (firstNext - entry.Addr);
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Error,
                        entry.Offset,
                        MessageList.MessageEntry.MessageType.BankOverrun,
                        string.Format(fmt, "+" + badOffset.ToString("x6")),
                        MessageList.MessageEntry.ProblemResolution.None));
                }
            }
        }

        /// <summary>
        /// Checks for hidden visualization sets.
        /// </summary>
        private void ValidateVisualizationSets() {
            foreach (KeyValuePair<int, VisualizationSet> kvp in VisualizationSets) {
                Anattrib attr = GetAnattrib(kvp.Key);
                if (!attr.IsStart) {
                    string tag = string.Empty;
                    if (kvp.Value.Count > 0) {
                        tag = kvp.Value[0].Tag;
                    }
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Warning,
                        kvp.Key,
                        MessageList.MessageEntry.MessageType.HiddenVisualization,
                        tag,
                        MessageList.MessageEntry.ProblemResolution.VisualizationIgnored));
                }
            }
        }

        #region Analysis

        /// <summary>
        /// Analyzes the file data.  This is the main entry point for code/data analysis.
        /// </summary>
        /// <param name="reanalysisRequired">How much work to do.</param>
        /// <param name="debugLog">Object to send debug output to.</param>
        /// <param name="reanalysisTimer">Task timestamp collection object.</param>
        public void Analyze(UndoableChange.ReanalysisScope reanalysisRequired,
                CommonUtil.DebugLog debugLog, TaskTimer reanalysisTimer) {
            // This method doesn't report failures.  It succeeds to the best of its ability,
            // and handles problems by discarding bad data.  The overall philosophy is that
            // the program will never generate bad data, and any bad project file contents
            // (possibly introduced by hand-editing) are identified at load time, called out
            // to the user, and discarded.
            //
            // We do want to collect the failures so we can present them to the user.
            Messages.Clear();

            Debug.Assert(reanalysisRequired != UndoableChange.ReanalysisScope.None);
            reanalysisTimer.StartTask("DisasmProject.Analyze()");

            // Populate the symbol table with platform symbols, in file load order, then
            // merge in the project symbols, potentially replacing platform symbols that
            // have the same label.  This version of the table is passed to plugins during
            // code analysis.
            reanalysisTimer.StartTask("SymbolTable init");
            SymbolTable.Clear();
            MergePlatformProjectSymbols();

            // Merge user labels into the symbol table, overwriting platform/project symbols
            // where they conflict.  Labels whose values are out of sync (because of a change
            // to the address map) are updated as part of this.
            UpdateAndMergeUserLabels();
            reanalysisTimer.EndTask("SymbolTable init");

            if (reanalysisRequired == UndoableChange.ReanalysisScope.CodeAndData) {
                // Always want to start with a blank array.  Going to be lazy and let the
                // system allocator handle that for us.
                // NOTE: don't generate any Messages during code analysis -- we clear the
                // list at the start of each pass, and we don't always analyze code.
                mAnattribs = new Anattrib[mFileData.Length];

                reanalysisTimer.StartTask("CodeAnalysis.Analyze");

                CodeAnalysis ca = new CodeAnalysis(mFileData, CpuDef, mAnattribs, AddrMap,
                    TypeHints, StatusFlagOverrides, ProjectProps.EntryFlags,
                    ProjectProps.AnalysisParams, mScriptManager, debugLog);

                ca.Analyze();
                reanalysisTimer.EndTask("CodeAnalysis.Analyze");

                // Save a copy of the current state.
                mCodeOnlyAnattribs = new Anattrib[mAnattribs.Length];
                Array.Copy(mAnattribs, mCodeOnlyAnattribs, mAnattribs.Length);
            } else {
                // Load Anattribs array from the stored copy.
                Debug.WriteLine("Partial reanalysis");
                reanalysisTimer.StartTask("CodeAnalysis (restore prev)");
                Debug.Assert(mCodeOnlyAnattribs != null);
                Array.Copy(mCodeOnlyAnattribs, mAnattribs, mAnattribs.Length);
                reanalysisTimer.EndTask("CodeAnalysis (restore prev)");
            }

            reanalysisTimer.StartTask("Apply labels, formats, etc.");
            // Apply any user-defined labels to the Anattribs array.
            ApplyUserLabels(debugLog);

            // Apply user-created format descriptors to instructions and data items.
            ApplyFormatDescriptors(debugLog);
            reanalysisTimer.EndTask("Apply labels, formats, etc.");

            reanalysisTimer.StartTask("DataAnalysis");
            DataAnalysis da = new DataAnalysis(this, mAnattribs);
            da.DebugLog = debugLog;

            reanalysisTimer.StartTask("DataAnalysis.AnalyzeDataTargets");
            da.AnalyzeDataTargets();
            reanalysisTimer.EndTask("DataAnalysis.AnalyzeDataTargets");

            // Analyze uncategorized regions.  When this completes, the Anattrib array will
            // be complete for every offset, and the file will be traversible by walking
            // through the lengths of each entry.
            reanalysisTimer.StartTask("DataAnalysis.AnalyzeUncategorized");
            da.AnalyzeUncategorized();
            reanalysisTimer.EndTask("DataAnalysis.AnalyzeUncategorized");

            reanalysisTimer.EndTask("DataAnalysis");

            reanalysisTimer.StartTask("RemoveHiddenLabels");
            RemoveHiddenLabels();
            reanalysisTimer.EndTask("RemoveHiddenLabels");


            // ----------
            // NOTE: we could add an additional re-analysis entry point here, that just deals with
            // platform symbols and xrefs, to be used after a change to project symbols.  We'd
            // need to check all existing refs to confirm that the symbol hasn't been removed.
            // Symbol updates are sufficiently infrequent that this probably isn't worthwhile.

            reanalysisTimer.StartTask("GenerateVariableRefs");
            // Generate references to variables.
            GenerateVariableRefs();
            reanalysisTimer.EndTask("GenerateVariableRefs");

            // NOTE: we could at this point apply platform address symbols as code labels, so
            // that locations in the code that correspond to well-known addresses would pick
            // up the appropriate label instead of getting auto-labeled.  It's unclear
            // whether this is desirable, especially if the user is planning to modify the
            // output later on, and it could mess things up if we start slapping
            // labels into the middle of data regions.  It's generally safer to treat
            // platform symbols as labels for constants and external references.  If somebody
            // finds an important use case we can revisit this; might merit a special type
            // of equate or section in the platform symbol definition file.

            reanalysisTimer.StartTask("GeneratePlatformSymbolRefs");
            // Generate references to platform and project external symbols.
            GeneratePlatformSymbolRefs();
            reanalysisTimer.EndTask("GeneratePlatformSymbolRefs");

            reanalysisTimer.StartTask("GenerateXrefs");
            // Generate cross-reference lists.
            mXrefs.Clear();
            GenerateXrefs();
            reanalysisTimer.EndTask("GenerateXrefs");

            // replace simple auto-labels ("L1234") with annotated versions ("WR_1234")
            if (ProjectProps.AutoLabelStyle != AutoLabel.Style.Simple) {
                reanalysisTimer.StartTask("AnnotateAutoLabels");
                AnnotateAutoLabels();
                reanalysisTimer.EndTask("AnnotateAutoLabels");
            }

            reanalysisTimer.StartTask("GenerateActiveDefSymbolList");
            // Generate the list of project/platform symbols that are being used.  This forms
            // the list of EQUates at the top of the file.  The active set is identified from
            // the cross-reference data.
            GenerateActiveDefSymbolList();
            reanalysisTimer.EndTask("GenerateActiveDefSymbolList");

#if DEBUG
            reanalysisTimer.StartTask("Validate");
            Validate();
            reanalysisTimer.EndTask("Validate");
#endif
            reanalysisTimer.StartTask("ErrorCheck");
            ValidateAddressMap();
            ValidateVisualizationSets();
            reanalysisTimer.EndTask("ErrorCheck");

            reanalysisTimer.EndTask("DisasmProject.Analyze()");
            //reanalysisTimer.DumpTimes("DisasmProject timers:", debugLog);

            debugLog.LogI("Analysis complete");
            //Problems.DebugDump();
            Debug.WriteLine(SymbolTable.ToString());
        }

        /// <summary>
        /// Applies user labels to the Anattribs array.  Symbols with stale Value fields will
        /// be replaced.
        /// </summary>
        /// <param name="genLog">Log for debug messages.</param>
        private void ApplyUserLabels(DebugLog genLog) {
            foreach (KeyValuePair<int, Symbol> kvp in UserLabels) {
                int offset = kvp.Key;
                if (offset < 0 || offset >= mAnattribs.Length) {
                    genLog.LogE("Invalid offset +" + offset.ToString("x6") +
                        "(label=" + kvp.Value.Label + ")");
                    continue;       // ignore this
                }

                if (mAnattribs[offset].Symbol != null) {
                    genLog.LogW("Multiple labels at offset +" + offset.ToString("x6") +
                        ": " + kvp.Value.Label + " / " + mAnattribs[offset].Symbol.Label);
                    continue;
                }

                int expectedAddr = kvp.Value.Value;
                Debug.Assert(expectedAddr == AddrMap.OffsetToAddress(offset));

                // Add direct reference to the UserLabels Symbol object.
                mAnattribs[offset].Symbol = kvp.Value;
            }
        }

        /// <summary>
        /// Applies user-defined format descriptors to the Anattribs array.  This specifies the
        /// format for instruction operands, and identifies data items.
        /// </summary>
        /// <remarks>
        /// In an ideal world, this would be a trivial function.  In practice it's possible for
        /// all sorts of weird edge cases to arise, e.g. if you hint something as data, apply
        /// formats, and then hint it as code, many strange things are possible.  We don't want
        /// to delete user data if it seems out of place, but we do want to ignore anything
        /// that's going to confuse the source generator later on.
        ///
        /// Problem reports are written to a log (which is shown by the Analyzer Output
        /// window) and the Problems list.  Once the latter is better established we can
        /// stop sending them to the log.
        /// </remarks>
        /// <param name="genLog">Log for debug messages.</param>
        private void ApplyFormatDescriptors(DebugLog genLog) {
            genLog.LogI("Applying format descriptors");

            // TODO(someday): move error format strings to string dictionary

            foreach (KeyValuePair<int, FormatDescriptor> kvp in OperandFormats) {
                int offset = kvp.Key;
                FormatDescriptor dfd = kvp.Value;

                // Check offset.
                if (offset < 0 || offset >= mFileData.Length) {
                    string msg = "invalid offset (desc=" + dfd + ")";
                    genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Error,
                        offset,
                        MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
                        msg,
                        MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                    Debug.Assert(false);
                    continue;
                }

                // Make sure it doesn't run off the end
                if (offset + dfd.Length > mFileData.Length) {
                    string msg = "invalid offset+len: len=" + dfd.Length +
                        " file=" + mFileData.Length;
                    genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Error,
                        offset,
                        MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
                        msg,
                        MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                    Debug.Assert(false);
                    continue;
                }

                if (!AddrMap.IsSingleAddrRange(offset, dfd.Length)) {
                    string msg = "descriptor straddles address change; len=" + dfd.Length;
                    genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Warning,
                        offset,
                        MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
                        msg,
                        MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                    continue;
                }

                if (mAnattribs[offset].IsInstructionStart) {
                    // Check length for instruction formatters.  This can happen if you format
                    // a bunch of bytes as single-byte data items and then add a code entry
                    // point.
                    if (dfd.Length != mAnattribs[offset].Length) {
                        string msg = "unexpected length on instr format descriptor (" +
                            dfd.Length + " vs " + mAnattribs[offset].Length + ")";
                        genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
                        Messages.Add(new MessageList.MessageEntry(
                            MessageList.MessageEntry.SeverityLevel.Warning,
                            offset,
                            MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
                            msg,
                            MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                        continue;
                    }
                    if (dfd.Length == 1) {
                        // No operand to format!
                        string msg = "unexpected format descriptor on single-byte op";
                        genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
                        Messages.Add(new MessageList.MessageEntry(
                            MessageList.MessageEntry.SeverityLevel.Warning,
                            offset,
                            MessageList.MessageEntry.MessageType.InvalidDescriptor,
                            msg,
                            MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                        continue;
                    }
                    if (!dfd.IsValidForInstruction) {
                        string msg = "descriptor not valid for instruction: " + dfd;
                        genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
                        Messages.Add(new MessageList.MessageEntry(
                            MessageList.MessageEntry.SeverityLevel.Warning,
                            offset,
                            MessageList.MessageEntry.MessageType.InvalidDescriptor,
                            msg,
                            MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                        continue;
                    }
                } else if (mAnattribs[offset].IsInstruction) {
                    // Mid-instruction format.
                    string msg = "unexpected mid-instruction format descriptor";
                    genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Warning,
                        offset,
                        MessageList.MessageEntry.MessageType.InvalidDescriptor,
                        msg,
                        MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                    continue;
                } else {
                    // Data or inline data.  The data analyzer hasn't run yet.  We want to
                    // confirm that the descriptor doesn't overlap with code.
                    //
                    // Data descriptors that overlap code are problematic, for two reasons.
                    // First, we end up with references to hidden labels, because the code that
                    // tries to prevent it sees an Anattrib with code at the target address and
                    // assumes all is well.  Second, if the overlap ends partway into an
                    // instruction, an Anattrib-walker will move from a data region to the middle
                    // of an instruction, which should never happen.
                    //
                    // All instruction bytes have been marked, so we just need to confirm that
                    // none of the bytes spanned by this descriptor are instructions.
                    bool overlap = false;
                    for (int i = offset; i < offset + dfd.Length; i++) {
                        if (mAnattribs[i].IsInstruction) {
                            string msg =
                                "data format descriptor overlaps code at +" + i.ToString("x6");
                            genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
                            Messages.Add(new MessageList.MessageEntry(
                                MessageList.MessageEntry.SeverityLevel.Warning,
                                offset,
                                MessageList.MessageEntry.MessageType.InvalidDescriptor,
                                msg,
                                MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                            overlap = true;
                            break;
                        }
                    }
                    if (overlap) {
                        continue;
                    }

#if false
                    // Check junk+align directive.  We don't outright reject the descriptor,
                    // because it still works as junk+unalign, but we want to add a warning
                    // message.
                    //
                    // NOTE: currently disabled because some relevant changes, such as editing
                    // a format descriptor sub-type, don't cause data re-analysis.  I don't think
                    // this is important enough to special-case or implement elsewhere.
                    if (dfd.IsAlignedJunk) {
                        if (!AsmGen.GenCommon.CheckJunkAlign(offset, dfd, AddrMap)) {
                            string msg = "junk alignment not valid";
                            Messages.Add(new MessageList.MessageEntry(
                                MessageList.MessageEntry.SeverityLevel.Info,
                                offset,
                                MessageList.MessageEntry.MessageType.InvalidDescriptor,
                                msg,
                                MessageList.MessageEntry.ProblemResolution.None));
                        }
                    }
#endif
                }

                // All tests passed.  Apply the descriptor.
                mAnattribs[offset].DataDescriptor = dfd;
            }
        }

        /// <summary>
        /// Merges symbols from PlatformSymbols and ProjectSymbols into SymbolTable.
        ///
        /// This should be done before any other symbol assignment or generation, so that user
        /// labels take precedence (by virtue of overwriting the earlier platform symbols),
        /// and auto label generation can propery generate a unique label.
        ///
        /// Within platform symbol loading, later symbols should replace earlier symbols,
        /// so that ordering of platform files behaves in an intuitive fashion.
        /// </summary>
        private void MergePlatformProjectSymbols() {
            // Start by pulling in the platform symbols.  The list in PlatformSymbols is in
            // order, so we can just overwrite earlier symbols with matching labels.
            foreach (PlatformSymbols ps in PlatformSyms) {
                foreach (Symbol sym in ps) {
                    if (sym.Value == PlatformSymbols.ERASE_VALUE) {
                        // "erase" value
                        if (SymbolTable.TryGetValue(sym.Label, out Symbol found)) {
                            SymbolTable.Remove(found);
                        }
                    } else {
                        // overwrite
                        SymbolTable[sym.Label] = sym;
                    }
                }
            }

            // Now add project symbols, overwriting platform symbols with the same label.
            foreach (KeyValuePair<string, DefSymbol> kvp in ProjectProps.ProjectSyms) {
                SymbolTable[kvp.Value.Label] = kvp.Value;
            }
        }

        /// <summary>
        /// Returns true if a symbol with a matching label appears in the project symbols
        /// or any of the platform lists.  This operates on the raw lists, not SymbolTable.
        /// </summary>
        /// <remarks>
        /// Useful for determining whether a label masks a project or platform symbol.
        /// </remarks>
        private bool IsInProjectOrPlatformList(Symbol sym) {
            if (sym == null) {
                return false;
            }
            foreach (PlatformSymbols ps in PlatformSyms) {
                if (ps.ContainsKey(sym.Label)) {
                    return true;
                }
            }
            if (ProjectProps.ProjectSyms.ContainsKey(sym.Label)) {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Merges symbols from UserLabels into SymbolTable.  Existing entries with matching
        /// labels will be replaced.
        /// </summary>
        /// <remarks>
        /// It might make sense to exclude non-unique labels, but that's probably better done
        /// with a UI filter option.
        /// </remarks>
        private void UpdateAndMergeUserLabels() {
            // We store symbols as label+value, but for a user label the actual value is
            // the address of the offset the label is associated with, which can change if
            // the user updates the address map.  It's convenient to store labels as Symbols
            // because we also want the Type value, and it avoids having to create Symbol
            // objects on the fly.  If the value in the user label is wrong, we fix it here.

            Dictionary<int, Symbol> changes = new Dictionary<int, Symbol>();

            foreach (KeyValuePair<int, Symbol> kvp in UserLabels) {
                int offset = kvp.Key;
                Symbol sym = kvp.Value;
                int expectedAddr = AddrMap.OffsetToAddress(offset);
                if (sym.Value != expectedAddr) {
                    Symbol newSym = sym.UpdateValue(expectedAddr);
                    Debug.WriteLine("Updating label value: " + sym + " --> " + newSym);
                    changes[offset] = newSym;
                    sym = newSym;
                }
                SymbolTable[kvp.Value.Label] = sym;
            }

            // If we updated any symbols, merge the changes back into UserLabels.
            if (changes.Count != 0) {
                Debug.WriteLine("...merging " + changes.Count + " symbols into UserLabels");
            }
            foreach (KeyValuePair<int, Symbol> kvp in changes) {
                UserLabels[kvp.Key] = kvp.Value;
            }
        }

        /// <summary>
        /// Removes user labels from the symbol table if they're in the middle of an
        /// instruction or multi-byte data area.  (Easy way to cause this: hint a 3-byte
        /// instruction as data, add a label to the middle byte, remove hints.)
        /// 
        /// Call this after the code and data analysis passes have completed.  Any
        /// references to the hidden labels will just fall through.  It will be possible
        /// to create multiple labels with the same name, because the app won't see them
        /// in the symbol table.
        /// </summary>
        private void RemoveHiddenLabels() {
            // TODO(someday): keep the symbols in the symbol table so we can't create a
            //   duplicate, but flag it as hidden.  The symbol resolver will need to know
            //   to ignore it.  Provide a way for users to purge them.  We could just blow
            //   them out of UserLabels right now, but I'm trying to avoid discarding user-
            //   created data without permission.
            foreach (KeyValuePair<int, Symbol> kvp in UserLabels) {
                int offset = kvp.Key;
                if (!mAnattribs[offset].IsStart) {
                    Debug.WriteLine("Stripping hidden label '" + kvp.Value.Label + "'");
                    SymbolTable.Remove(kvp.Value);

                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Warning,
                        offset,
                        MessageList.MessageEntry.MessageType.HiddenLabel,
                        kvp.Value.Label,
                        MessageList.MessageEntry.ProblemResolution.LabelIgnored));
                }
            }
        }

        /// <summary>
        /// Generates references to symbols in the local variable tables.
        ///
        /// These only apply to instructions with a specific set of addressing modes.
        ///
        /// This must be called after the code and data analysis passes have completed.  It
        /// should run before project/platform symbol references are generated, since we want
        /// variables to take precedence.
        ///
        /// This also adds all symbols in non-hidden variable tables to the main SymbolTable,
        /// for the benefit of future uniqueness checks.
        /// </summary>
        private void GenerateVariableRefs() {
            LocalVariableLookup lvLookup = new LocalVariableLookup(LvTables, this,
                null, false, false);

            for (int offset = 0; offset < FileData.Length; ) {
                // Was a table defined at this offset?
                List<DefSymbol> vars = lvLookup.GetVariablesDefinedAtOffset(offset);
                if (vars != null) {
                    // All entries also get added to the main SymbolTable.  This is a little
                    // wonky because the symbol might already exist with a different value.
                    // So long as the previous thing was also a variable, it doesn't matter.
                    foreach (DefSymbol defSym in vars) {
                        if (!SymbolTable.TryGetValue(defSym.Label, out Symbol sym)) {
                            // Symbol not yet in symbol table.  Add it.
                            //
                            // NOTE: if you try to run the main app with uniqification enabled,
                            // this will cause the various uniquified forms of local variables
                            // to end up in the main symbol table.  This can cause clashes with
                            // user labels that would not occur otherwise.
                            SymbolTable[defSym.Label] = defSym;
                        } else if (!sym.IsVariable) {
                            // Somehow we have a variable and a non-variable with the same
                            // name.  Platform/project symbols haven't been processed yet, so
                            // this must be a clash with a user label.  This could cause
                            // assembly source gen to fail later on.  It's possible to do this
                            // by "hiding" a table and then adding a user label, so we can't just
                            // fix it at project load time.
                            //
                            // This is now handled by the LvLookup code, which renames the
                            // duplicate label, so we shouldn't get here.
                            Debug.WriteLine("Found non-variable with var name in symbol table: "
                                + sym);
                            Debug.Assert(false);
                        }
                    }
                } else if (LvTables.TryGetValue(offset, out LocalVariableTable unused)) {
                    // table was ignored
                    Messages.Add(new MessageList.MessageEntry(
                        MessageList.MessageEntry.SeverityLevel.Warning,
                        offset,
                        MessageList.MessageEntry.MessageType.HiddenLocalVariableTable,
                        string.Empty,
                        MessageList.MessageEntry.ProblemResolution.LocalVariableTableIgnored));
                }

                Anattrib attr = mAnattribs[offset];
                if (attr.IsInstructionStart && attr.DataDescriptor == null) {
                    OpDef op = CpuDef.GetOpDef(FileData[offset]);
                    DefSymbol defSym = null;
                    if (op.IsDirectPageInstruction) {
                        Debug.Assert(attr.OperandAddress == FileData[offset + 1]);
                        defSym = lvLookup.GetSymbol(offset, FileData[offset + 1],
                            Symbol.Type.ExternalAddr);
                    } else if (op.IsStackRelInstruction) {
                        defSym = lvLookup.GetSymbol(offset, FileData[offset + 1],
                            Symbol.Type.Constant);
                    }
                    if (defSym != null) {
                        WeakSymbolRef vref = new WeakSymbolRef(defSym.Label,
                            WeakSymbolRef.Part.Low, op.IsStackRelInstruction ?
                                WeakSymbolRef.LocalVariableType.StackRelConst :
                                WeakSymbolRef.LocalVariableType.DpAddr);
                        mAnattribs[offset].DataDescriptor =
                            FormatDescriptor.Create(attr.Length, vref, false);
                    }
                }

                if (attr.IsDataStart || attr.IsInlineDataStart) {
                    offset += attr.Length;
                } else {
                    // Advance by one, not attr.Length, so we don't miss embedded instructions.
                    offset++;
                }
            }
        }

        /// <summary>
        /// Generates references to symbols in the project/platform symbol tables.
        /// 
        /// For each instruction or data item that appears to reference an address, and
        /// does not have a target offset, look for a matching address in the symbol tables.
        /// 
        /// This works pretty well for addresses, but is a little rough for constants.
        /// 
        /// Call this after the code and data analysis passes have completed.  This doesn't
        /// interact with labels, so the ordering there doesn't matter.  This should come after
        /// local variable resolution, so that those have priority.
        /// </summary>
        private void GeneratePlatformSymbolRefs() {
            bool checkNearby = ProjectProps.AnalysisParams.SeekNearbyTargets;

            for (int offset = 0; offset < mAnattribs.Length; ) {
                Anattrib attr = mAnattribs[offset];
                Symbol sym;
                int address;
                OpDef.MemoryEffect accType = OpDef.MemoryEffect.Unknown;
                if (attr.IsInstructionStart && attr.DataDescriptor == null &&
                        attr.OperandAddress >= 0 && attr.OperandOffset < 0) {
                    // This is an instruction that hasn't been explicitly formatted.  It
                    // has an operand address, but not an offset, meaning it's a reference
                    // to an address outside the scope of the file. See if it has a
                    // platform symbol definition.
                    //
                    // It might seem unwise to examine the full symbol table, because it has
                    // non-project non-platform symbols in it.  However, any matching user
                    // labels would have been applied already.  Also, we want to ensure that
                    // conflicting user labels take precedence, e.g. creating a user label "COUT"
                    // will prevent a platform symbol with the same name from being visible.
                    // Using the full symbol table is potentially a tad less efficient than
                    // looking for a match exclusively in project/platform symbols, but it's
                    // the correct thing to do.
                    OpDef op = CpuDef.GetOpDef(FileData[offset]);
                    accType = op.MemEffect;
                    address = attr.OperandAddress;
                    sym = SymbolTable.FindNonVariableByAddress(address, accType);
                } else if ((attr.IsDataStart || attr.IsInlineDataStart) &&
                        attr.DataDescriptor != null && attr.DataDescriptor.IsNumeric &&
                        attr.DataDescriptor.FormatSubType == FormatDescriptor.SubType.Address) {
                    // Found a Numeric/Address data item that matches.  Data items don't have
                    // OperandAddress or OperandOffset set, so we need to check manually to
                    // see if the address falls within the project.  In most situations this
                    // isn't really necessary, because the data analysis pass will have resolved
                    // interal references to auto-generated labels.
                    //
                    // This is only firing if the item is explicitly formatted as an
                    // Address, so we're essentially "upgrading" the user format.
                    address = RawData.GetWord(mFileData, offset, attr.DataDescriptor.Length,
                        attr.DataDescriptor.FormatType == FormatDescriptor.Type.NumericBE);
                    if (AddrMap.AddressToOffset(offset, address) < 0) {
                        accType = OpDef.MemoryEffect.ReadModifyWrite;   // guess
                        sym = SymbolTable.FindNonVariableByAddress(address, accType);
                    } else {
                        Debug.WriteLine("Found unhandled internal data addr ref at +" +
                            offset.ToString("x6"));
                        address = -1;       // don't touch interior stuff
                        sym = null;
                    }
                } else {
                    address = -1;
                    sym = null;
                }

                if (address >= 0) {
                    // If we didn't find it, see if addr+1 has a label.  Sometimes indexed
                    // addressing will use "STA addr-1,y".  This will also catch "STA addr-1"
                    // when addr is the very start of a segment, which means we're actually
                    // finding a label reference rather than project/platform symbol; only
                    // works if the location already has a label.
                    //
                    // Don't do this for zero-page locations, because those are usually
                    // individual variables that aren't accessed via addr-1.  There are
                    // exceptions, but more often than not it's just distracting.
                    if (sym == null && checkNearby && (address & 0xffff) < 0xffff &&
                            address > 0x0000ff) {
                        sym = SymbolTable.FindNonVariableByAddress(address + 1, accType);
                        if (sym != null && sym.SymbolSource != Symbol.Source.Project &&
                                sym.SymbolSource != Symbol.Source.Platform) {
                            Debug.WriteLine("Applying non-platform in GeneratePlatform: " + sym);
                            // should be okay to do this
                        }
                    }

                    // If we found something, and it's not a variable, create a descriptor.
                    if (sym != null && !sym.IsVariable) {
                        mAnattribs[offset].DataDescriptor =
                            FormatDescriptor.Create(mAnattribs[offset].Length,
                                new WeakSymbolRef(sym.Label, WeakSymbolRef.Part.Low), false);
                    }
                }

                if (attr.IsDataStart || attr.IsInlineDataStart) {
                    offset += attr.Length;
                } else {
                    // Advance by one, not attr.Length, so we don't miss embedded instructions.
                    offset++;
                }
            }
        }

        /// <summary>
        /// Generates labels for branch and data targets, and xref lists for all referenced
        /// offsets.  Also generates Xref entries for DefSymbols (for .eq directives).
        /// 
        /// Call this after the code and data analysis passes have completed.
        /// </summary>
        private void GenerateXrefs() {
            // Xref generation.  There are two general categories of references:
            //  (1) Numeric reference.  Comes from instructions (e.g. "LDA $1000" or "BRA $1000")
            //      and Numeric/Address data items.
            //  (2) Symbolic reference.  Comes from instructions and data with Symbol format
            //      descriptors.  In some cases this may be a partial ref, e.g. "LDA #>label".
            //      The symbol's value may not match the operand, in which case an adjustment
            //      is applied.
            //
            // We want to tag both.  So if "LDA $1000" becomes "LDA label-2", we want to
            // add a numeric reference to the code at $1000, and a symbolic reference to the
            // labe at $1002, that point back to the LDA instruction.  These are presented
            // slightly differently to the user.  For a symbolic reference with no adjustment,
            // we don't add the (redundant) numeric reference.
            //
            // In some cases the numeric reference will land in the middle of an instruction
            // or multi-byte data area and won't be visible.

            // Clear previous cross-reference data from project/platform symbols.  These
            // symbols don't have file offsets, so we can't store them in the main mXrefs
            // list.
            // TODO(someday): DefSymbol is otherwise immutable.  We should put these elsewhere,
            //   maybe a Dictionary<DefSymbol, XrefSet>?  Just mind the garbage collection.
            foreach (Symbol sym in SymbolTable) {
                if (sym is DefSymbol) {
                    (sym as DefSymbol).Xrefs.Clear();
                }
            }
            // TODO: seriously, put the XrefSet ref somewhere else
            foreach (LocalVariableTable lvt in LvTables.Values) {
                for (int i = 0; i < lvt.Count; i++) {
                    lvt[i].Xrefs.Clear();
                }
            }

            // Create a mapping from label (which must be unique) to file offset.  This
            // is different from UserLabels (which only has user-created labels, and is
            // sorted by offset) and SymbolTable (which has constants and platform symbols,
            // and uses the address as value rather than the offset).
            SortedList<string, int> labelList = new SortedList<string, int>(mFileData.Length,
                Asm65.Label.LABEL_COMPARER);
            for (int offset = 0; offset < mAnattribs.Length; offset++) {
                Anattrib attr = mAnattribs[offset];
                if (attr.Symbol != null) {
                    try {
                        labelList.Add(attr.Symbol.Label, offset);
                    } catch (ArgumentException ex) {
                        // Duplicate UserLabel entries are stripped when projects are loaded,
                        // but it might be possible to cause this by hiding/unhiding a
                        // label (e.g. using hints to place it in the middle of an instruction).
                        // Just ignore the duplicate.
                        Debug.WriteLine("Xref ignoring duplicate label '" + attr.Symbol.Label +
                            "': " + ex.Message);
                    }
                }
            }

            // No particular reason to do this here, but it's convenient.
            ByteCounts.Reset();

            LocalVariableLookup lvLookup = new LocalVariableLookup(LvTables, this,
                null, false, false);

            // Walk through the Anattrib array, adding xref entries to things referenced
            // by the entity at the current offset.
            for (int offset = 0; offset < mAnattribs.Length; ) {
                Anattrib attr = mAnattribs[offset];

                XrefSet.XrefType xrefType = XrefSet.XrefType.Unknown;
                OpDef.MemoryEffect accType = OpDef.MemoryEffect.Unknown;
                bool isIndexedDirect = false;
                if (attr.IsInstruction) {
                    OpDef op = CpuDef.GetOpDef(FileData[offset]);
                    if (op.IsSubroutineCall) {
                        xrefType = XrefSet.XrefType.SubCallOp;
                    } else if (op.IsBranchOrSubCall) {
                        xrefType = XrefSet.XrefType.BranchOp;
                    } else {
                        xrefType = XrefSet.XrefType.MemAccessOp;
                        accType = op.MemEffect;
                        isIndexedDirect = op.IsIndexedAccessInstruction;
                    }
                } else if (attr.IsData || attr.IsInlineData) {
                    xrefType = XrefSet.XrefType.RefFromData;
                }

                bool hasZeroOffsetSym = false;
                if (attr.DataDescriptor != null) {
                    FormatDescriptor dfd = attr.DataDescriptor;
                    if (dfd.FormatSubType == FormatDescriptor.SubType.Symbol) {
                        // For instructions with address operands that resolve in-file, grab
                        // the target offset.
                        int operandOffset = -1;
                        if (attr.IsInstructionStart) {
                            operandOffset = attr.OperandOffset;
                        }

                        // Is this a reference to a label?
                        if (labelList.TryGetValue(dfd.SymbolRef.Label, out int symOffset)) {
                            // Compute adjustment.
                            int adj = 0;
                            if (operandOffset >= 0) {
                                // We can compute (symOffset - operandOffset), but that gives us
                                // the offset adjustment, not the address adjustment.
                                adj = mAnattribs[symOffset].Address -
                                    mAnattribs[operandOffset].Address;
                            }

                            AddXref(symOffset, new XrefSet.Xref(offset, true, xrefType, accType,
                                isIndexedDirect, adj));
                            if (adj == 0) {
                                hasZeroOffsetSym = true;
                            }
                        } else if (dfd.SymbolRef.IsVariable) {
                            DefSymbol defSym = lvLookup.GetSymbol(offset, dfd.SymbolRef);
                            if (defSym != null) {
                                int adj = 0;
                                if (operandOffset >= 0) {
                                    adj = defSym.Value - operandOffset;
                                }
                                defSym.Xrefs.Add(new XrefSet.Xref(offset, true, xrefType, accType,
                                    isIndexedDirect, adj));
                            }
                        } else if (SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) {
                            // Is this a reference to a project/platform symbol?
                            if (sym.SymbolSource == Symbol.Source.Project ||
                                    sym.SymbolSource == Symbol.Source.Platform) {
                                DefSymbol defSym = sym as DefSymbol;
                                int adj = 0;
                                // NOTE: operandOffset may be valid, since you are allowed to
                                // define project symbols for addresses inside the file.  I
                                // don't think we need to fiddle with that though.
                                //Debug.Assert(operandOffset < 0);
                                if (sym.SymbolType != Symbol.Type.Constant &&
                                        attr.OperandAddress >= 0) {
                                    // It's an address operand, so we can compute the offset.
                                    adj = defSym.Value - attr.OperandAddress;
                                } else {
                                    // We could compute the operand's value and display
                                    // the difference, so "LDA #$00" --> "LDA #FOO-1" when
                                    // FOO is 1 would display "FOO -1" in the xref table.
                                    // Not sure if that's useful.
                                    //
                                    // We would need to shift the value to match the part,
                                    // e.g. "LDA #>BLAH" would grab the high part.  We'd need
                                    // to tweak the adjustment math appropriately.
                                }
                                defSym.Xrefs.Add(new XrefSet.Xref(offset, true, xrefType, accType,
                                    isIndexedDirect, adj));
                            } else {
                                // Can get here if somebody creates an address operand symbol
                                // that refers to a local variable.
                                Debug.WriteLine("NOTE: not xrefing +" + offset.ToString("x6") +
                                    " " + sym);
                            }
                        } else {
                            // Reference to non-existent symbol.
                            Messages.Add(new MessageList.MessageEntry(
                                MessageList.MessageEntry.SeverityLevel.Info,
                                offset,
                                MessageList.MessageEntry.MessageType.UnresolvedWeakRef,
                                dfd.SymbolRef.Label,
                                MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
                        }
                    } else if (dfd.FormatSubType == FormatDescriptor.SubType.Address) {
                        // not expecting this format on an instruction operand
                        Debug.Assert(attr.IsData || attr.IsInlineData);

                        // This generally doesn't happen for internal addresses, because
                        // we create an auto label for the target address, and a weak ref
                        // to the auto label, which means the xref is handled by the symbol
                        // code above.  This case really only happens for external addresses,
                        // which either have a label (because we defined a symbol) and got
                        // handled earlier, or don't have a label and aren't useful for a
                        // cross-reference.
                        //
                        // There might be a case I'm missing, so I'm going to take a swing
                        // at it and spit out a debug message either way.
                        int operandAddr = RawData.GetWord(mFileData, offset,
                            dfd.Length, dfd.FormatType == FormatDescriptor.Type.NumericBE);
                        int targetOffset = AddrMap.AddressToOffset(offset, operandAddr);
                        if (targetOffset < 0) {
                            Debug.WriteLine("No xref for addr $" + operandAddr.ToString("x4") +
                                " at +" + offset.ToString("x6"));
                        } else {
                            Debug.WriteLine("HEY: found unlabeled addr ref at +" +
                                offset.ToString("x6"));
                            AddXref(targetOffset, new XrefSet.Xref(offset, false, xrefType,
                                accType, isIndexedDirect, 0));
                        }
                    }

                    // Look for instruction offset references.  We skip this if we've already
                    // added a reference from a symbol with zero adjustment, since that would
                    // just leave a duplicate entry.  (The symbolic ref wins because we need
                    // it for the label localizer and possibly the label refactorer.)
                    if (!hasZeroOffsetSym && attr.IsInstructionStart && attr.OperandOffset >= 0) {
                        AddXref(attr.OperandOffset, new XrefSet.Xref(offset, false, xrefType,
                            accType, isIndexedDirect, 0));
                    }
                }

                if (attr.IsDataStart || attr.IsInlineDataStart) {
                    // There shouldn't be data items inside of other data items.
                    offset += attr.Length;

                    if (attr.DataDescriptor != null &&
                            attr.DataDescriptor.FormatType == FormatDescriptor.Type.Junk) {
                        ByteCounts.JunkByteCount += attr.Length;
                    } else {
                        ByteCounts.DataByteCount += attr.Length;
                    }
                } else {
                    // Advance by one, not attr.Length, so we don't miss embedded instructions.
                    offset++;

                    ByteCounts.CodeByteCount++;
                }
            }
        }

        /// <summary>
        /// Adds an Xref entry to an XrefSet in mXrefs.  The XrefSet will be created if necessary.
        /// </summary>
        /// <param name="offset">File offset for which cross-references are being noted.</param>
        /// <param name="xref">Cross reference to add to the set.</param>
        private void AddXref(int offset, XrefSet.Xref xref) {
            if (!mXrefs.TryGetValue(offset, out XrefSet xset)) {
                xset = mXrefs[offset] = new XrefSet();
            }
            xset.Add(xref);
        }

        /// <summary>
        /// Returns the XrefSet for the specified offset.  May return null if the set is
        /// empty.
        /// </summary>
        public XrefSet GetXrefSet(int offset) {
            mXrefs.TryGetValue(offset, out XrefSet xset);
            return xset;        // will be null if not found
        }

        /// <summary>
        /// Replaces generic auto-labels with fancier versions generated from xrefs.
        /// </summary>
        private void AnnotateAutoLabels() {
            AutoLabel.Style style = ProjectProps.AutoLabelStyle;
            Debug.Assert(style != AutoLabel.Style.Simple);

            // We don't have a list of auto labels, so just pick them out of anattribs.
            for (int offset = 0; offset < mAnattribs.Length; offset++) {
                Anattrib attr = mAnattribs[offset];
                if (attr.Symbol != null && attr.Symbol.SymbolSource == Symbol.Source.Auto) {
                    XrefSet xset = GetXrefSet(offset);
                    if (xset == null) {
                        // Nothing useful to do here. This is unexpected, since auto-labels
                        // should only exist because something referenced the offset.
                        continue;
                    }
                    Symbol newSym =
                        AutoLabel.GenerateAnnotatedLabel(attr.Address, SymbolTable, xset, style);
                    if (!newSym.Equals(attr.Symbol)) {
                        //Debug.WriteLine("Replace " + attr.Symbol.Label + " with " +newSym.Label);

                        // Replace the symbol in Anattribs, update the symbol table, then
                        // call Refactor to update everything that referenced it.
                        Symbol oldSym = mAnattribs[offset].Symbol;
                        mAnattribs[offset].Symbol = newSym;
                        SymbolTable.Remove(oldSym);
                        SymbolTable.Add(newSym);
                        RefactorLabel(offset, oldSym.Label);
                    }
                }
            }
        }

        /// <summary>
        /// Generates the list of project/platform symbols that are being used.  Any
        /// DefSymbol with a non-empty Xrefs is included.  Previous contents are cleared.
        /// 
        /// The list is sorted primarily by value, secondarily by symbol name, with constants
        /// appearing before addresses.
        /// 
        /// Call this after Xrefs are generated.
        /// </summary>
        private void GenerateActiveDefSymbolList() {
            ActiveDefSymbolList.Clear();

            foreach (Symbol sym in SymbolTable) {
                if (!(sym is DefSymbol) || sym.IsVariable) {
                    continue;
                }
                DefSymbol defSym = sym as DefSymbol;
                if (defSym.Xrefs.Count == 0) {
                    continue;
                }
                ActiveDefSymbolList.Add(defSym);
            }

            // Sort order:
            // - constants appear before addresses
            // - ascending numeric value, wider items first
            // - ascending label
            ActiveDefSymbolList.Sort(delegate (DefSymbol a, DefSymbol b) {
                // Put constants first.
                int ca = (a.IsConstant) ? 1 : 0;
                int cb = (b.IsConstant) ? 1 : 0;
                if (ca != cb) {
                    return cb - ca;
                }

                if (a.Value < b.Value) {
                    return -1;
                } else if (a.Value > b.Value) {
                    return 1;
                } else if (DefSymbol.IsWider(a, b)) {
                    return -1;
                } else if (DefSymbol.IsWider(b, a)) {
                    return 1;
                }
                return Asm65.Label.LABEL_COMPARER.Compare(a.Label, b.Label);
            });
        }

        #endregion Analysis


        #region Change Management

        /// <summary>
        /// Generates a ChangeSet that merges the FormatDescriptors in the new list into
        /// OperandFormats.
        /// 
        /// All existing descriptors that overlap with new descriptors will be removed.
        /// In cases where old and new descriptors have the same starting offset, this
        /// will be handled with a single change object.
        /// 
        /// If old and new descriptors are identical, no change object will be generated.
        /// It's possible for this to return an empty change set.
        /// </summary>
        /// <param name="newList">List of new format descriptors.</param>
        /// <returns>Change set.</returns>
        public ChangeSet GenerateFormatMergeSet(SortedList<int, FormatDescriptor> newList) {
            Debug.WriteLine("Generating format merge set...");
            ChangeSet cs = new ChangeSet(newList.Count * 2);

            // The Keys and Values properties are documented to return the internal data
            // structure, not make a copy, so this will be fast.
            IList<int> mainKeys = OperandFormats.Keys;
            IList<FormatDescriptor> mainValues = OperandFormats.Values;
            IList<int> newKeys = newList.Keys;
            IList<FormatDescriptor> newValues = newList.Values;

            // The basic idea is to walk through the new list, checking each entry for
            // conflicts with the main list.  If there's no conflict, we create a change
            // object for the new item.  If there is a conflict, we resolve it appropriately.
            //
            // The check on the main list is very fast because both lists are in sorted
            // order, so we can just walk the main list forward.  If a main-list entry
            // conflicts, we create a removal object, and advance the main index.
            int mainIndex = 0;
            int newIndex = 0;
            while (newIndex < newKeys.Count) {
                int newOffset = newKeys[newIndex];
                int newLength = newValues[newIndex].Length;
                if (mainIndex >= mainKeys.Count) {
                    // We've run off the end of the main list.  Just add the new item.
                    UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
                        newOffset, null, newValues[newIndex]);
                    cs.AddNonNull(uc);
                    newIndex++;
                    continue;
                }

                // Check for overlap by computing the intersection.  Start and end form two
                // points; the intersection is the largest of the start points and the
                // smallest of the end points.  If the result of the computation puts end before
                // start, there's no overlap.
                int mainOffset = mainKeys[mainIndex];
                int mainLength = mainValues[mainIndex].Length;
                Debug.Assert(newLength > 0 && mainLength > 0);
                int interStart = Math.Max(mainOffset, newOffset);
                int interEnd = Math.Min(mainOffset + mainLength, newOffset + newLength);
                // exclusive end point, so interEnd == interStart means no overlap
                if (interEnd > interStart) {
                    Debug.WriteLine("Found overlap: main(+" + mainOffset.ToString("x6") +
                        "," + mainLength + ") : new(+" + newOffset.ToString("x6") +
                        "," + newLength + ")");

                    // See if the initial offsets are identical.  If so, put the add and
                    // remove into a single change.  This isn't strictly necessary, but it's
                    // slightly more efficient.
                    if (mainOffset == newOffset) {
                        // Check to see if the descriptors are identical.  If so, ignore this.
                        if (mainValues[mainIndex] == newValues[newIndex]) {
                            Debug.WriteLine(" --> no-op change " + newValues[newIndex]);
                        } else {
                            Debug.WriteLine(" --> replace change " + newValues[newIndex]);
                            UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
                                newOffset, mainValues[mainIndex], newValues[newIndex]);
                            cs.AddNonNull(uc);
                        }
                    } else {
                        // Remove the old entry, add the new entry.
                        Debug.WriteLine(" --> remove/add change " + newValues[newIndex]);
                        UndoableChange ruc = UndoableChange.CreateActualOperandFormatChange(
                            mainOffset, mainValues[mainIndex], null);
                        UndoableChange auc = UndoableChange.CreateActualOperandFormatChange(
                            newOffset, null, newValues[newIndex]);
                        cs.AddNonNull(ruc);
                        cs.AddNonNull(auc);
                    }
                    newIndex++;

                    // Remove all other main-list entries that overlap with this one.
                    while (++mainIndex < mainKeys.Count) {
                        mainOffset = mainKeys[mainIndex];
                        mainLength = mainValues[mainIndex].Length;
                        interStart = Math.Max(mainOffset, newOffset);
                        interEnd = Math.Min(mainOffset + mainLength, newOffset + newLength);
                        // exclusive end point, so interEnd == interStart means no overlap
                        if (interEnd <= interStart) {
                            break;
                        }
                        Debug.WriteLine(" also remove +" + mainOffset.ToString("x6") +
                            mainValues[mainIndex]);
                        UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
                            mainOffset, mainValues[mainIndex], null);
                        cs.AddNonNull(uc);
                    }
                } else {
                    // No overlap.  If the main entry is earlier, we can cross it off the list
                    // and advance to the next one.  Otherwise, we add the change and advance
                    // that list.
                    if (mainOffset < newOffset) {
                        mainIndex++;
                    } else {
                        Debug.WriteLine("Add non-overlap " + newOffset.ToString("x6") +
                            newValues[newIndex]);
                        UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
                            newOffset, null, newValues[newIndex]);
                        cs.AddNonNull(uc);
                        newIndex++;
                    }
                }
            }

            // Trim away excess capacity, since this will probably be sitting in an undo
            // list for a long time.
            cs.TrimExcess();
            Debug.WriteLine("Total " + cs.Count + " changes");
            return cs;
        }

        /// <summary>
        /// Returns the analyzer attributes for the specified byte offset.
        /// 
        /// Bear in mind that Anattrib is a struct, and thus the return value is a copy.
        /// </summary>
        public Anattrib GetAnattrib(int offset) {
            return mAnattribs[offset];
        }

        /// <summary>
        /// Returns true if the offset has a long comment, note, or visualization.  Used for
        /// determining how to split up a data area.
        /// Currently not returning true for an end-of-line comment.
        /// </summary>
        /// <param name="offset">Offset of interest.</param>
        /// <returns>True if a comment, note, or visualization was found.</returns>
        public bool HasCommentNoteOrVis(int offset) {
            return (LongComments.ContainsKey(offset) ||
                    Notes.ContainsKey(offset) ||
                    VisualizationSets.ContainsKey(offset));
        }

        /// <summary>
        /// True if an "undo" operation is available.
        /// </summary>
        public bool CanUndo { get { return mUndoTop > 0; } }

        /// <summary>
        /// True if a "redo" operation is available.
        /// </summary>
        public bool CanRedo { get { return mUndoTop < mUndoList.Count; } }

        /// <summary>
        /// True if something has changed since the last time the file was saved.
        /// </summary>
        public bool IsDirty { get { return mUndoTop != mUndoSaveIndex; } }

        /// <summary>
        /// Sets the save index equal to the undo position.  Do this after the file has
        /// been successfully saved.
        /// </summary>
        public void ResetDirtyFlag() {
            mUndoSaveIndex = mUndoTop;
        }

        /// <summary>
        /// Returns the next undo operation, and moves the pointer to the previous item.
        /// </summary>
        public ChangeSet PopUndoSet() {
            if (!CanUndo) {
                throw new Exception("Can't undo");
            }
            Debug.WriteLine("PopUndoSet: returning entry " + (mUndoTop - 1) + ": " +
                mUndoList[mUndoTop - 1]);
            return mUndoList[--mUndoTop];
        }

        /// <summary>
        /// Returns the next redo operation, and moves the pointer to the next item.
        /// </summary>
        /// <returns></returns>
        public ChangeSet PopRedoSet() {
            if (!CanRedo) {
                throw new Exception("Can't redo");
            }
            Debug.WriteLine("PopRedoSet: returning entry " + mUndoTop + ": " +
                mUndoList[mUndoTop]);
            return mUndoList[mUndoTop++];
        }

        /// <summary>
        /// Adds a change set to the undo list.  All redo operations above it on the
        /// stack are removed.
        /// 
        /// We currently allow empty sets.
        /// </summary>
        /// <param name="changeSet">Set to push.</param>
        public void PushChangeSet(ChangeSet changeSet) {
            if (IsReadOnly) {
                return;
            }
            Debug.WriteLine("PushChangeSet: adding " + changeSet);

            // Remove all of the "redo" entries from the current position to the end.
            if (mUndoTop < mUndoList.Count) {
                Debug.WriteLine("PushChangeSet: removing " + (mUndoList.Count - mUndoTop) +
                    " entries");
                mUndoList.RemoveRange(mUndoTop, mUndoList.Count - mUndoTop);
            }

            mUndoList.Add(changeSet);
            mUndoTop = mUndoList.Count;

            // If the user makes a change, saves the file, hits undo, then makes another change,
            // the "undo top" and "save index" will be equal, which will make us think the
            // file doesn't need to be saved.  In reality there is no longer any undo index that
            // matches the saved file state.
            if (mUndoSaveIndex >= mUndoTop) {
                mUndoSaveIndex = -1;
            }
        }

        /// <summary>
        /// Returns the change at the top of the list, i.e. the one that would be popped off
        /// if we hit "undo".
        /// </summary>
        /// <returns>The change set.  The caller must not modify it.</returns>
        public ChangeSet GetTopChange() {
            Debug.Assert(mUndoList.Count > 0);
            Debug.Assert(mUndoTop > 0);
            return mUndoList[mUndoTop - 1];
        }

        /// <summary>
        /// Generate undo/redo history, for the debug window.
        /// </summary>
        public string DebugGetUndoRedoHistory() {
            StringBuilder sb = new StringBuilder();
            sb.Append("Bracketed change will be overwritten by next action\r\n\r\n");

            for (int i = 0; i < mUndoList.Count; i++) {
                ChangeSet cs = mUndoList[i];

                char lbr, rbr;
                if (i == mUndoTop) {
                    lbr = '[';
                    rbr = ']';
                } else {
                    lbr = rbr = ' ';
                }
                sb.AppendFormat("{0}{3,3:D}{1}{2}: {4} change{5}\r\n",
                    lbr, rbr, i == mUndoSaveIndex ? "*" : " ",
                    i, cs.Count, cs.Count == 1 ? "" : "s");

                for (int j = 0; j < cs.Count; j++) {
                    UndoableChange uc = cs[j];
                    sb.AppendFormat("       type={0} offset=+{1} reReq={2}\r\n",
                        uc.Type, uc.HasOffset ? uc.Offset.ToString("x6") : "N/A",
                        uc.ReanalysisRequired);
                }
            }
            if (mUndoTop == mUndoList.Count) {
                sb.AppendFormat("[ - ]{0}\r\n", mUndoTop == mUndoSaveIndex ? "*" : " ");
            }

            return sb.ToString();
        }

        /// <summary>
        /// Applies the changes to the project, and updates the display.
        /// </summary>
        /// <param name="cs">Set of changes to apply.</param>
        /// <param name="backward">If set, undo the changes instead.</param>
        /// <param name="affectedOffsets">List of offsets affected by change.  Only meaningful
        ///   when the result is not "None".</param>
        /// <returns>An indication of the level of reanalysis required.  If this returns None,
        ///   the list of offsets to update will be in affectedOffsets.</returns>
        public UndoableChange.ReanalysisScope ApplyChanges(ChangeSet cs, bool backward,
                out RangeSet affectedOffsets) {
            affectedOffsets = new RangeSet();
            if (IsReadOnly) {
                return UndoableChange.ReanalysisScope.None;
            }

            UndoableChange.ReanalysisScope needReanalysis = UndoableChange.ReanalysisScope.None;

            // TODO(maybe): if changes overlap, we need to apply them in reverse order when
            //   "backward" is set.  This requires a reverse-order enumerator from
            //   ChangeSet.  Not currently needed.
            foreach (UndoableChange uc in cs) {
                object oldValue, newValue;

                // Unpack change, flipping old/new for undo.
                if (!backward) {
                    oldValue = uc.OldValue;
                    newValue = uc.NewValue;
                } else {
                    oldValue = uc.NewValue;
                    newValue = uc.OldValue;
                }
                int offset = uc.Offset;

                switch (uc.Type) {
                    case UndoableChange.ChangeType.Dummy:
                        //if (uc.ReanalysisRequired == UndoableChange.ReanalysisFlags.None) {
                        //    affectedOffsets.AddRange(0, FileData.Length - 1);
                        //}
                        break;
                    case UndoableChange.ChangeType.SetAddress: {
                            AddressMap addrMap = AddrMap;
                            if (addrMap.Get(offset) != (int)oldValue) {
                                Debug.WriteLine("GLITCH: old address value mismatch (" +
                                    addrMap.Get(offset) + " vs " + (int)oldValue + ")");
                                Debug.Assert(false);
                            }
                            addrMap.Set(offset, (int)newValue);

                            Debug.WriteLine("Map offset +" + offset.ToString("x6") + " to $" +
                                ((int)newValue).ToString("x6"));

                            // Visualization generators in extension scripts could be chasing
                            // addresses.  Give them a chance to update.
                            ClearVisualizationCache();

                            // ignore affectedOffsets
                            Debug.Assert(uc.ReanalysisRequired ==
                                UndoableChange.ReanalysisScope.CodeAndData);
                        }
                        break;
                    case UndoableChange.ChangeType.SetTypeHint: {
                            // Always requires full code+data re-analysis.
                            ApplyTypeHints((TypedRangeSet)oldValue, (TypedRangeSet)newValue);
                            // ignore affectedOffsets
                            Debug.Assert(uc.ReanalysisRequired ==
                                UndoableChange.ReanalysisScope.CodeAndData);
                        }
                        break;
                    case UndoableChange.ChangeType.SetStatusFlagOverride: {
                            if (StatusFlagOverrides[offset] != (StatusFlags)oldValue) {
                                Debug.WriteLine("GLITCH: old status flag mismatch (" +
                                    StatusFlagOverrides[offset] + " vs " +
                                    (StatusFlags)oldValue + ")");
                                Debug.Assert(false);
                            }
                            StatusFlagOverrides[offset] = (StatusFlags)newValue;
                            // ignore affectedOffsets
                            Debug.Assert(uc.ReanalysisRequired ==
                                UndoableChange.ReanalysisScope.CodeAndData);
                        }
                        break;
                    case UndoableChange.ChangeType.SetLabel: {
                            // NOTE: this is about managing changes to UserLabels.  Adding
                            // or removing a user-defined label requires a full reanalysis,
                            // even if there was already an auto-generated label present,
                            // so we don't need to undo/redo Anattribs for anything except
                            // for renaming a user-defined label.
                            UserLabels.TryGetValue(offset, out Symbol oldSym);
                            if (oldSym != (Symbol) oldValue) {
                                Debug.WriteLine("GLITCH: old label value mismatch ('" +
                                    oldSym + "' vs '" + oldValue + "')");
                                Debug.Assert(false);
                            }

                            if (newValue == null) {
                                // We're removing a user label.
                                UserLabels.Remove(offset);
                                SymbolTable.Remove((Symbol)oldValue);   // unnecessary? will regen
                                Debug.Assert(uc.ReanalysisRequired ==
                                    UndoableChange.ReanalysisScope.DataOnly);
                            } else {
                                // We're adding or renaming a user label.
                                //
                                // We should not be changing a label to the same value as an
                                // existing label -- the dialog should have prevented it.
                                // This is important because, if we edit a label to match an
                                // auto-generated label, we'll have a duplicate label unless we
                                // do a full code+data reanalysis.  If we're okay with reanalyzing
                                // on user-label renames, we can allow such conflicts.
                                //
                                // We might be changing it to match an existing platform symbol
                                // though.  (Ex: create label FOO, add .sym65 with symbol FOO,
                                // edit FOO to BAR, then hit undo.)
                                if (oldValue != null) {
                                    SymbolTable.Remove((Symbol)oldValue);
                                }
                                UserLabels[offset] = (Symbol)newValue;
                                SymbolTable[((Symbol)newValue).Label] = (Symbol)newValue;
                                //SymbolTable.Add((Symbol)newValue);
                                Debug.Assert(oldSym != null || uc.ReanalysisRequired ==
                                    UndoableChange.ReanalysisScope.DataOnly);
                            }

                            if (uc.ReanalysisRequired == UndoableChange.ReanalysisScope.None) {
                                // Shouldn't really be "changing" from null to null, but
                                // it's legal, so don't blow up if it happens.
                                // (The assert on SymbolSource is older -- we now only care about
                                // what's in UserLabels, which are always Source=User.)
                                Debug.Assert((oldValue == null && newValue == null) ||
                                    (((Symbol)oldValue).SymbolSource == Symbol.Source.User &&
                                     ((Symbol)newValue).SymbolSource == Symbol.Source.User));
                                // Not doing a full refresh, so keep this up to date.
                                mAnattribs[offset].Symbol = (Symbol)newValue;

                                if (oldValue != null) {
                                    // Update everything in Anattribs and OperandFormats that
                                    // referenced the old symbol.
                                    RefactorLabel(offset, ((Symbol)oldValue).Label);
                                }

                                // Compute the affected offsets.  We have one special case to
                                // consider: if we renamed a label, and the old or new name is
                                // in project or platform symbols, we need to restore that
                                // symbol to the symbol table.  The most reliable way to do that
                                // is to switch us to a data re-analysis.
                                //
                                // The UI doesn't let you directly edit a label to overwrite a
                                // symbol, but see FOO/BAR example above.
                                if (IsInProjectOrPlatformList((Symbol)oldValue) ||
                                        IsInProjectOrPlatformList((Symbol)newValue)) {
                                    Debug.WriteLine("Label change masked/unmasked " +
                                        "project/platform symbol");
                                    needReanalysis |= UndoableChange.ReanalysisScope.DataOnly;
                                } else {
                                    affectedOffsets.Add(offset);

                                    // Use the cross-reference table to identify the offsets that
                                    // we need to update.
                                    if (mXrefs.TryGetValue(offset, out XrefSet xrefs)) {
                                        foreach (XrefSet.Xref xr in xrefs) {
                                            // This isn't quite right -- in theory we should be
                                            // adding all offsets that are part of the instruction,
                                            // so that affectedOffsets can hold a contiguous range
                                            // instead of a collection of opcode offsets.  In
                                            // practice, for a label change, it shouldn't matter.
                                            affectedOffsets.Add(xr.Offset);
                                        }
                                    }
                                }
                            } else {
                                // We're not calling RefactorLabel() here because we should
                                // only be doing the reanalysis if we're adding or removing
                                // the label, not renaming it.  If that changes, we'll need
                                // to do the refactor here, though we can skip Anattribs work.
                                Debug.Assert(oldValue == null || newValue == null);
                            }

                            // For add/edit/remove, we need to see if what we do will impact
                            // the behavior of a plugin.  We don't need to do this on
                            // project/platform symbol changes because project property changes
                            // always update code and data.
                            if (mScriptManager.IsLabelSignificant((Symbol)oldValue,
                                    (Symbol)newValue)) {
                                Debug.WriteLine("Plugin claims symbol is significant");
                                needReanalysis |= UndoableChange.ReanalysisScope.CodeAndData;
                            }
                        }
                        break;
                    case UndoableChange.ChangeType.SetOperandFormat: {
                            // Note this is used for data/inline-data as well as instructions.
                            OperandFormats.TryGetValue(offset, out FormatDescriptor current);
                            if (current != (FormatDescriptor)oldValue) {
                                Debug.WriteLine("GLITCH: old operand format mismatch (" +
                                    current + " vs " + oldValue + ")");
                                Debug.Assert(false);
                            }
                            if (newValue == null) {
                                OperandFormats.Remove(offset);
                                mAnattribs[offset].DataDescriptor = null;
                            } else {
                                OperandFormats[offset] = mAnattribs[offset].DataDescriptor =
                                    (FormatDescriptor)newValue;
                            }
                            if (uc.ReanalysisRequired == UndoableChange.ReanalysisScope.None) {
                                // Add every offset in the range.  The length might be changing
                                // (e.g. an offset with a single byte is now the start of a
                                // 10-byte string), so touch everything that was affected by
                                // the old descriptor or is affected by the new descriptor.
                                // [This may no longer be necessary -- size changes now
                                // require reanalysis.]
                                int afctLen = 1;
                                if (oldValue != null) {
                                    afctLen =
                                        Math.Max(afctLen, ((FormatDescriptor)oldValue).Length);
                                }
                                if (newValue != null) {
                                    afctLen =
                                        Math.Max(afctLen, ((FormatDescriptor)newValue).Length);
                                }

                                for (int i = offset; i < offset + afctLen; i++) {
                                    affectedOffsets.Add(i);
                                }
                            }
                        }
                        break;
                    case UndoableChange.ChangeType.SetComment: {
                            if (!Comments[offset].Equals(oldValue)) {
                                Debug.WriteLine("GLITCH: old comment value mismatch ('" +
                                    Comments[offset] + "' vs '" + oldValue + "')");
                                Debug.Assert(false);
                            }
                            Comments[offset] = (string)newValue;

                            // Only affects this offset.
                            affectedOffsets.Add(offset);
                        }
                        break;
                    case UndoableChange.ChangeType.SetLongComment: {
                            LongComments.TryGetValue(offset, out MultiLineComment current);
                            if (current != (MultiLineComment)oldValue) {
                                Debug.WriteLine("GLITCH: old long comment value mismatch ('" +
                                    current + "' vs '" + oldValue + "')");
                                Debug.Assert(false);
                            }
                            if (newValue == null) {
                                LongComments.Remove(offset);
                            } else {
                                LongComments[offset] = (MultiLineComment)newValue;
                            }

                            // Only affects this offset.
                            affectedOffsets.Add(offset);
                        }
                        break;
                    case UndoableChange.ChangeType.SetNote: {
                            Notes.TryGetValue(offset, out MultiLineComment current);
                            if (current != (MultiLineComment)oldValue) {
                                Debug.WriteLine("GLITCH: old note value mismatch ('" +
                                    current + "' vs '" + oldValue + "')");
                                Debug.Assert(false);
                            }
                            if (newValue == null) {
                                Notes.Remove(offset);
                            } else {
                                Notes[offset] = (MultiLineComment)newValue;
                            }

                            // Only affects this offset.
                            affectedOffsets.Add(offset);
                        }
                        break;
                    case UndoableChange.ChangeType.SetProjectProperties: {
                            bool needPlatformSymReload = !CommonUtil.Container.StringListEquals(
                                ((ProjectProperties)oldValue).PlatformSymbolFileIdentifiers,
                                ((ProjectProperties)newValue).PlatformSymbolFileIdentifiers,
                                null /*StringComparer.InvariantCulture*/);
                            bool needExtScriptReload = !CommonUtil.Container.StringListEquals(
                                ((ProjectProperties)oldValue).ExtensionScriptFileIdentifiers,
                                ((ProjectProperties)newValue).ExtensionScriptFileIdentifiers,
                                null);

                            // ProjectProperties are mutable, so create a new object that's
                            // a clone of the one that will live in the undo buffer.
                            ProjectProps = new ProjectProperties((ProjectProperties)newValue);

                            // Most of the properties are simply used during the reanalysis
                            // process.  This must be set explicitly.  NOTE: replacing this
                            // could cause cached data (such as Formatter strings) to be
                            // discarded, so ideally we wouldn't do this unless we know the
                            // CPU definition has changed (or we know that GetBestMatch is
                            // memoizing results and will return the same object).
                            Debug.WriteLine("Replacing CPU def object");
                            UpdateCpuDef();

                            if (needPlatformSymReload || needExtScriptReload) {
                                string errMsgs = LoadExternalFiles();

                                // TODO(someday): if the plugin failed to compile, we will have
                                //   one or more error messages, which we are currently discarding
                                //   because we can't create UI here.  We either need a "change
                                //   messages" feature, or we need to pre-flight the plugin and
                                //   report the failure elsewhere.  (We also want a manual
                                //   "reload all external files and plugins" command, which might
                                //   run through here.)
                            }
                            if (needExtScriptReload) {
                                ClearVisualizationCache();
                            }
                        }
                        // ignore affectedOffsets
                        Debug.Assert(uc.ReanalysisRequired ==
                            UndoableChange.ReanalysisScope.CodeAndData);
                        break;
                    case UndoableChange.ChangeType.SetLocalVariableTable: {
                            LvTables.TryGetValue(offset, out LocalVariableTable current);
                            if (current != (LocalVariableTable)oldValue) {
                                Debug.WriteLine("GLITCH: old lvt value mismatch: current=" +
                                    current + " old=" + (LocalVariableTable)oldValue);
                                Debug.Assert(false);
                            }
                            if (newValue == null) {
                                LvTables.Remove(offset);
                            } else {
                                LvTables[offset] = (LocalVariableTable)newValue;
                            }
                            // ignore affectedOffsets
                            Debug.Assert(uc.ReanalysisRequired ==
                                UndoableChange.ReanalysisScope.DataOnly);
                        }
                        break;
                    case UndoableChange.ChangeType.SetVisualizationSet: {
                            VisualizationSets.TryGetValue(offset, out VisualizationSet current);
                            if (current != (VisualizationSet)oldValue) {
                                Debug.WriteLine("GLITCH: old visSet value mismatch: current=" +
                                    current + " old=" + (VisualizationSet)oldValue);
                                Debug.Assert(false);
                            }
                            if (newValue == null) {
                                VisualizationSets.Remove(offset);
                            } else {
                                VisualizationSets[offset] = (VisualizationSet)newValue;
                            }
                            Debug.Assert(uc.ReanalysisRequired ==
                                UndoableChange.ReanalysisScope.DisplayOnly);
                        }
                        break;
                    default:
                        break;
                }
                needReanalysis |= uc.ReanalysisRequired;
            }

            return needReanalysis;
        }

        /// <summary>
        /// Clears all visualization cached images.
        /// </summary>
        private void ClearVisualizationCache() {
            foreach (KeyValuePair<int, VisualizationSet> kvp in VisualizationSets) {
                kvp.Value.RefreshNeeded();
            }
        }

        /// <summary>
        /// Updates all symbolic references to the old label.  Call this after replacing
        /// mAnattribs[labelOffset].Symbol.
        /// </summary>
        /// <param name="labelOffset">Offset with the just-renamed label.</param>
        /// <param name="oldLabel">Previous value.</param>
        private void RefactorLabel(int labelOffset, string oldLabel) {
            if (!mXrefs.TryGetValue(labelOffset, out XrefSet xrefs)) {
                // This can happen if you add a label in a file section that nothing references,
                // and then rename it.
                Debug.WriteLine("RefactorLabel: no references to " + oldLabel);
                return;
            }

            string newLabel = mAnattribs[labelOffset].Symbol.Label;

            //
            // Update format descriptors in Anattribs.
            //
            foreach (XrefSet.Xref xr in xrefs) {
                FormatDescriptor dfd = mAnattribs[xr.Offset].DataDescriptor;
                if (dfd == null) {
                    // Should be a data target reference here?  Where'd the xref come from?
                    Debug.Assert(false);
                    continue;
                }
                if (!dfd.HasSymbol) {
                    // The auto-gen stuff would have created a symbol, but the user can
                    // override that and display as e.g. hex.
                    continue;
                }
                if (!Label.LABEL_COMPARER.Equals(oldLabel, dfd.SymbolRef.Label)) {
                    // This can happen if the xref is based on the operand offset,
                    // but the user picked a different symbol.  The xref generator
                    // creates entries for both target offsets, but only one will
                    // have a matching label.
                    continue;
                }

                mAnattribs[xr.Offset].DataDescriptor = FormatDescriptor.Create(
                    dfd.Length, new WeakSymbolRef(newLabel, dfd.SymbolRef.ValuePart),
                    dfd.FormatType == FormatDescriptor.Type.NumericBE);
            }

            //
            // Update value in OperandFormats.
            //
            foreach (XrefSet.Xref xr in xrefs) {
                if (!OperandFormats.TryGetValue(xr.Offset, out FormatDescriptor dfd)) {
                    // Probably an auto-generated symbol ref, so no entry in OperandFormats.
                    continue;
                }
                if (!dfd.HasSymbol) {
                    continue;
                }
                if (!Label.LABEL_COMPARER.Equals(oldLabel, dfd.SymbolRef.Label)) {
                    continue;
                }

                Debug.WriteLine("Replacing OpFor symbol at +" + xr.Offset.ToString("x6") +
                    " with " + newLabel);
                OperandFormats[xr.Offset] = FormatDescriptor.Create(
                    dfd.Length, new WeakSymbolRef(newLabel, dfd.SymbolRef.ValuePart),
                    dfd.FormatType == FormatDescriptor.Type.NumericBE);
            }
        }


        /// <summary>
        /// Applies the values in the set to the project hints.
        /// </summary>
        /// <param name="oldSet">Previous values; must match current contents.</param>
        /// <param name="newSet">Values to apply.</param>
        private void ApplyTypeHints(TypedRangeSet oldSet, TypedRangeSet newSet) {
            CodeAnalysis.TypeHint[] hints = TypeHints;
            foreach (TypedRangeSet.Tuple tuple in newSet) {
                CodeAnalysis.TypeHint curType = hints[tuple.Value];
                if (!oldSet.GetType(tuple.Value, out int oldType) || oldType != (int)curType) {
                    Debug.WriteLine("Type mismatch at " + tuple.Value);
                    Debug.Assert(false);
                }

                //Debug.WriteLine("Set +" + tuple.Value.ToString("x6") + " to " +
                //    (CodeAnalysis.TypeHint)tuple.Type + " (was " +
                //    curType + ")");

                hints[tuple.Value] = (CodeAnalysis.TypeHint)tuple.Type;
            }
        }

        #endregion Change Management

        /// <summary>
        /// Finds a label by name.  SymbolTable must be populated.
        /// </summary>
        /// <param name="name">Label to find.</param>
        /// <returns>File offset associated with label, or -1 if not found.</returns>
        public int FindLabelOffsetByName(string name) {
            // We're interested in user labels and auto-generated labels.  Do a lookup in
            // SymbolTable to find the symbol, then if it's user or auto, we do a second
            // search to find the file offset it's associated with.  The second search
            // requires a linear walk through anattribs; if we do this often we'll want to
            // maintain a symbol-to-offset structure.
            //
            // This will not find "hidden" labels, i.e. labels that are in the middle of an
            // instruction or multi-byte data area, because those are removed from SymbolTable.

            if (!SymbolTable.TryGetValue(name, out Symbol sym)) {
                return -1;
            }
            if (!sym.IsInternalLabel) {
                return -1;
            }
            for (int i = 0; i < mAnattribs.Length; i++) {
                if (mAnattribs[i].Symbol == sym) {
                    return i;
                }
            }
            Debug.WriteLine("NOTE: symbol '" + name + "' exists, but wasn't found in labels");
            return -1;
        }

        /// <summary>
        /// Finds a user label by name, searching only non-unique local address labels.
        /// </summary>
        /// <param name="label">Label to search for.  Must not have the uniquifier tag (if
        ///   it had one, you wouldn't be here).</param>
        /// <param name="targetOffset">If multiple labels are found, we want the one that is
        ///   closest to this offset.</param>
        /// <returns>The symbol found, or null if no match.</returns>
        public Symbol FindBestNonUniqueLabel(string label, int targetOffset) {
            Symbol bestSym = null;
            int bestDelta = int.MaxValue;

            // Simple linear search.  Right now we're only doing this in a few specific
            // UI-driven situations (edit operand, goto label), so performance isn't crucial.
            foreach (KeyValuePair<int, Symbol> kvp in UserLabels) {
                Symbol sym = kvp.Value;
                if (sym.SymbolType != Symbol.Type.NonUniqueLocalAddr) {
                    continue;
                }
                if (sym.LabelWithoutTag == label) {
                    // found a match; is it the best one?
                    int delta = Math.Abs(kvp.Key - targetOffset);
                    if (delta < bestDelta) {
                        //Debug.WriteLine("FindBest: " + sym.Label + "/" + delta + " better than " +
                        //    (bestSym != null ? bestSym.Label : "-") + "/" + bestDelta);
                        bestSym = sym;
                        bestDelta = delta;
                    } else {
                        //Debug.WriteLine("FindBest: " + sym.Label + "/" + delta + " not better than " +
                        //    (bestSym != null ? bestSym.Label : "-") + "/" + bestDelta);
                    }
                }
            }

            return bestSym;
        }

        // Punch-through functions; trying to avoid exposing ScriptManager for now.
        public Dictionary<string, PluginCommon.IPlugin> GetActivePlugins() {
            return mScriptManager.GetActivePlugins();
        }
        public PluginCommon.IPlugin GetPlugin(string scriptIdent) {
            return mScriptManager.GetInstance(scriptIdent);
        }
        public void PrepareScripts(PluginCommon.IApplication appRef) {
            mScriptManager.PrepareScripts(appRef);
        }
        public void UnprepareScripts() {
            mScriptManager.UnprepareScripts();
        }

        /// <summary>
        /// For debugging purposes, get some information about the currently loaded
        /// extension scripts.
        /// </summary>
        public string DebugGetLoadedScriptInfo() {
            return mScriptManager.DebugGetLoadedScriptInfo();
        }
    }
}
