/// <summary>
        /// Formats a strongly-typed field value in a format suitable for writing to the 2DA file,
        /// according to the schema for the corresponding column.
        /// </summary>
        /// <param name="value">The value to format.</param>
        /// <param name="schemaColumn">The schema of the column from which the value was taken.</param>
        /// <returns></returns>
        private static string FormatFieldValue(object value, TwoDASchema.Column schemaColumn)
        {
            if (value == null || value is DBNull)
            {
                return(BlankEntry);
            }

            if (value is int && schemaColumn != null && schemaColumn.DataType == TwoDASchema.DataType.HexInteger)
            {
                return("0x" + ((int)value).ToString("X" + schemaColumn.Digits));
            }

            string result = Convert.ToString(value, CultureInfo.InvariantCulture);

            result = result.Replace('"', '\'');
            if (result.Contains(' '))
            {
                result = '"' + result + '"';
            }

            return(result);
        }
        /// <summary>
        /// Loads 2DA data from the specified read.
        /// </summary>
        /// <param name="reader">The reader to read the data from.</param>
        /// <param name="lineNumberCallback">
        /// The line number callback. For each line read from the reader, the callback will
        /// be invoked once, with the number of the line read passed as an argument.
        /// </param>
        /// <exception cref="InvalidDataException">
        /// 2DA data in the reader is not in a correct format, is corrupted, or does not match
        /// the schema.
        /// </exception>
        public void Load(TextReader reader, Action <int> lineNumberCallback)
        {
            int lineNumber = 0;

            if (reader == null)
            {
                throw new ArgumentNullException("reader");
            }

            try
            {
                isLoading = true;

                Data.Clear();
                Data.Columns.Clear();

                string signatureLine = reader.ReadLine().TrimEnd();
                ++lineNumber;

                if (signatureLine != validSignature)
                {
                    throw new InvalidDataException(string.Format(
                                                       "Error at line {0}: '{1}' is not a valid 2DA signature",
                                                       lineNumber,
                                                       signatureLine));
                }

                if (lineNumberCallback != null)
                {
                    lineNumberCallback(lineNumber);
                }

                string defaultValueLine = reader.ReadLine().TrimEnd();
                ++lineNumber;

                if (defaultValueLine == null)
                {
                    throw new InvalidDataException(string.Format(
                                                       "Error at line {0}: default value line is missing",
                                                       lineNumber));
                }

                if (defaultValueLine.Length != 0)
                {
                    if (!defaultValueLine.StartsWith(defaultValueMarker))
                    {
                        throw new InvalidDataException(string.Format(
                                                           "Error at line {0}: default value line must either be blank, or begin with '{1}'",
                                                           lineNumber,
                                                           defaultValueMarker));
                    }
                    DefaultString = defaultValueLine.Remove(0, defaultValueMarker.Length).TrimStart();
                }

                if (lineNumberCallback != null)
                {
                    lineNumberCallback(lineNumber);
                }

                string columnNamesLine = null;
                do
                {
                    columnNamesLine = reader.ReadLine().TrimEnd();
                    ++lineNumber;
                } while (columnNamesLine == "");

                if (columnNamesLine == null)
                {
                    throw new InvalidDataException(string.Format(
                                                       "Error at line {0}: column names line is missing",
                                                       lineNumber));
                }

                var rowNumberColumn = new DataColumn("#", typeof(int));
                rowNumberColumn.AllowDBNull = true;
                Data.Columns.Add(rowNumberColumn);

                var columnNames = GetFieldValues(columnNamesLine);
                foreach (string columnName in columnNames)
                {
                    var column = new DataColumn(columnName, typeof(string));

                    TwoDASchema.Column schemaColumn = null;
                    if (Schema != null && Schema.Columns != null)
                    {
                        schemaColumn = Schema.Columns.FirstOrDefault(c => c.Name == columnName);
                    }

                    if (schemaColumn == null)
                    {
                        column.DataType     = typeof(string);
                        column.MaxLength    = 267;
                        column.AllowDBNull  = true;
                        column.DefaultValue = DBNull.Value;
                    }
                    else
                    {
                        column.AllowDBNull = schemaColumn.AllowBlanks;
                        switch (schemaColumn.DataType)
                        {
                        case TwoDASchema.DataType.String:
                            column.DataType  = typeof(string);
                            column.MaxLength = 267;
                            break;

                        case TwoDASchema.DataType.Float:
                            column.DataType = typeof(float);
                            break;

                        case TwoDASchema.DataType.Integer:
                        case TwoDASchema.DataType.HexInteger:
                        case TwoDASchema.DataType.StrRef:
                            column.DataType = typeof(int);
                            break;
                        }
                    }

                    Data.Columns.Add(column);
                }
                FillSchemaColumns();

                if (lineNumberCallback != null)
                {
                    lineNumberCallback(lineNumber);
                }

                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    ++lineNumber;
                    line = line.TrimEnd();
                    if (line.Length == 0)
                    {
                        continue;
                    }

                    DataRow row = Data.NewRow();

                    IEnumerable <string> fieldValues = GetFieldValues(line);
                    int columnIndex = 0;
                    foreach (string fieldValue in fieldValues)
                    {
                        if (columnIndex >= Data.Columns.Count)
                        {
                            throw new InvalidDataException(string.Format(
                                                               "Error at line {0}: too many field values",
                                                               lineNumber));
                        }

                        object actualValue =
                            fieldValue == BlankEntry ?
                            (object)DBNull.Value :
                            fieldValue;

                        var schemaColumn = schemaColumns[columnIndex];
                        if (schemaColumn != null && actualValue != DBNull.Value)
                        {
                            switch (schemaColumn.DataType)
                            {
                            case TwoDASchema.DataType.Integer:
                            case TwoDASchema.DataType.StrRef:
                            {
                                actualValue = int.Parse(fieldValue, CultureInfo.InvariantCulture);
                            } break;

                            case TwoDASchema.DataType.HexInteger:
                            {
                                if (fieldValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
                                {
                                    actualValue = int.Parse(fieldValue.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                                }
                                else
                                {
                                    actualValue = int.Parse(fieldValue, CultureInfo.InvariantCulture);
                                }
                            } break;

                            case TwoDASchema.DataType.Float:
                            {
                                actualValue = float.Parse(fieldValue, CultureInfo.InvariantCulture);
                            } break;
                            }
                        }

                        try
                        {
                            row[columnIndex] = actualValue;
                        }
                        catch (ArgumentException ex)
                        {
                            throw new InvalidDataException(string.Format(
                                                               "Error at line {0}: {1}",
                                                               lineNumber,
                                                               ex.Message), ex);
                        }

                        ++columnIndex;
                    }

                    Data.Rows.Add(row);

                    if (lineNumberCallback != null)
                    {
                        lineNumberCallback(lineNumber);
                    }
                }

                Data.AcceptChanges();
                IsModified = false;
            }
            catch (DataException ex)
            {
                throw new InvalidDataException(string.Format(
                                                   "Error at line {0}: {1}",
                                                   lineNumber,
                                                   ex.Message), ex);
            }
            finally
            {
                isLoading = false;
            }

            undoStack.Clear();
            redoStack.Clear();

            OnPropertyChanged(new PropertyChangedEventArgs("CanUndo"));
            OnPropertyChanged(new PropertyChangedEventArgs("CanRedo"));
        }