/// <summary>
        /// The user has deleted a row
        /// </summary>
        private void grdSplices_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e)
        {
            if (e.Row.ReadOnly)
            {
                // It was one of the originals
                int deletedRowIdx = e.Row.Index;

                double?loss = null;
                if (null != grdSplices[colLoss.Index, deletedRowIdx].Value)
                {
                    // We know that since the row is original and non-null, the value must have come straight out of the
                    // database and be parsable
                    loss = double.Parse(grdSplices[colLoss.Index, deletedRowIdx].Value.ToString());
                }

                object type = grdSplices[colLoss.Index + 1, deletedRowIdx].Value;

                List <Range> aRanges = SpliceAndConnectionUtils.ParseRanges(grdSplices[colRangeA.Index, deletedRowIdx].Value.ToString());
                List <Range> bRanges = SpliceAndConnectionUtils.ParseRanges(grdSplices[colRangeB.Index, deletedRowIdx].Value.ToString());

                List <Connection> connections = SpliceAndConnectionUtils.MatchUp(aRanges, bRanges);
                foreach (Connection connection in connections)
                {
                    Range aRange = connection.ARange;
                    Range bRange = connection.BRange;

                    int numUnits = aRange.High - aRange.Low + 1;
                    for (int offset = 0; offset < numUnits; offset++)
                    {
                        int aUnit = aRange.Low + offset;
                        int bUnit = bRange.Low + offset;

                        FiberSplice deletedSplice = new FiberSplice(new Range(aUnit, aUnit), new Range(bUnit, bUnit), loss, type);
                        _deleted[aUnit] = deletedSplice;
                    }

                    if (lblAvailableA.Text.StartsWith("N"))
                    {
                        lblAvailableA.Text = string.Format("Available: {0}", aRange);
                    }
                    else
                    {
                        lblAvailableA.Text += string.Format(",{0}", aRange.ToString());
                    }

                    if (lblAvailableB.Text.StartsWith("N"))
                    {
                        lblAvailableB.Text = string.Format("Available: {0}", bRange);
                    }
                    else
                    {
                        lblAvailableB.Text += string.Format(",{0}", bRange.ToString());
                    }
                }
            }

            btnSave.Enabled = true;
            btnSave.Tag     = true; // Edits made
        }
        /// <summary>
        /// The user has deleted a row
        /// </summary>
        private void grdConnections_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e)
        {
            if (e.Row.ReadOnly)
            {
                // It was one of the originals
                int deletedRowIdx = grdConnections.CurrentRow.Index;

                List <Range> aRanges = SpliceAndConnectionUtils.ParseRanges(grdConnections[colFromRange.Index, deletedRowIdx].Value.ToString());
                List <Range> bRanges = SpliceAndConnectionUtils.ParseRanges(grdConnections[colToRange.Index, deletedRowIdx].Value.ToString());

                List <Connection> connections = SpliceAndConnectionUtils.MatchUp(aRanges, bRanges);

                foreach (Connection connection in connections)
                {
                    Range aRange = connection.ARange;
                    Range bRange = connection.BRange;

                    int numUnits = aRange.High - aRange.Low + 1;
                    for (int offset = 0; offset < numUnits; offset++)
                    {
                        _deleted[aRange.Low + offset] = bRange.Low + offset;
                    }

                    if (lblAvailableFrom.Text.StartsWith("N"))
                    {
                        lblAvailableFrom.Text = string.Format("Available: {0}", aRange);
                    }
                    else
                    {
                        lblAvailableFrom.Text += string.Format(",{0}", aRange.ToString());
                    }

                    if (lblAvailableTo.Text.StartsWith("N"))
                    {
                        lblAvailableTo.Text = string.Format("Available: {0}", bRange);
                    }
                    else
                    {
                        lblAvailableTo.Text += string.Format(",{0}", bRange.ToString());
                    }
                }
            }

            btnSave.Enabled = true;
            btnSave.Tag     = true; // Edits have been made
        }
        /// <summary>
        /// Check changes to the grid and save them to the database
        /// </summary>
        /// <param name="splice">The associated splice closure</param>
        /// <param name="cableA">A cable</param>
        /// <param name="cableB">The other cable</param>
        /// <returns>Success</returns>
        private bool SaveChanges(SpliceClosureWrapper splice, FiberCableWrapper cableA, SpliceableCableWrapper cableB)
        {
            bool   result        = false;
            string isNotOkString = string.Empty;

            Dictionary <int, FiberSplice> currentGrid = new Dictionary <int, FiberSplice>();
            List <int> currentBStrands = new List <int>();

            try
            {
                int aIdx    = colRangeA.Index;
                int bIdx    = colRangeB.Index;
                int lossIdx = colLoss.Index;
                int typeIdx = grdSplices.Columns[colType.Name].Index; // If we had to use colTypeText, it will be using the same name

                // Less than count-1 lets us avoid the insert row
                for (int i = 0; i < grdSplices.Rows.Count - 1; i++)
                {
                    if (grdSplices[aIdx, i].Value == null || grdSplices[bIdx, i].Value == null)
                    {
                        isNotOkString = "A or B unit range missing.";
                    }
                    if (0 < isNotOkString.Length)
                    {
                        // No need to check the rest if this one was not OK
                        break;
                    }

                    List <Range> aRanges = SpliceAndConnectionUtils.ParseRanges(grdSplices[aIdx, i].Value.ToString());
                    List <Range> bRanges = SpliceAndConnectionUtils.ParseRanges(grdSplices[bIdx, i].Value.ToString());

                    if (!SpliceAndConnectionUtils.AreCountsEqual(aRanges, bRanges))
                    {
                        isNotOkString = "Number of units from A to B must match on each row.";
                    }
                    else if (!SpliceAndConnectionUtils.AreRangesWithinFiberCount(aRanges, cableA))
                    {
                        isNotOkString = "Selected units exceed fiber count for cable A.";
                    }
                    else if (!SpliceAndConnectionUtils.AreRangesWithinFiberCount(bRanges, cableB))
                    {
                        isNotOkString = "Selected units exceed fiber count for cable B.";
                    }

                    if (0 < isNotOkString.Length)
                    {
                        // No need to check the rest if this one was not OK
                        break;
                    }

                    List <Connection> matchedUp = SpliceAndConnectionUtils.MatchUp(aRanges, bRanges);
                    foreach (Connection range in matchedUp)
                    {
                        Range a        = range.ARange;
                        Range b        = range.BRange;
                        int   numUnits = a.High - a.Low + 1;
                        for (int offset = 0; offset < numUnits; offset++)
                        {
                            int aUnit = a.Low + offset;
                            int bUnit = b.Low + offset;

                            if (currentGrid.ContainsKey(aUnit))
                            {
                                isNotOkString = string.Format("Duplicate splicing found for A Strand {0}", aUnit);
                                // No need to check the rest if this one was not OK
                                break;
                            }
                            else if (currentBStrands.Contains(bUnit))
                            {
                                isNotOkString = string.Format("Duplicate splicing found for B Strand {0}", bUnit);
                                // No need to check the rest if this one was not OK
                                break;
                            }
                            else
                            {
                                object lossObj = grdSplices[lossIdx, i].Value;
                                object typeObj = grdSplices[typeIdx, i].Value;

                                double?loss = null;
                                if (null != lossObj)
                                {
                                    double dblLoss = -1;
                                    if (double.TryParse(lossObj.ToString(), out dblLoss))
                                    {
                                        loss = dblLoss;
                                    }
                                    else
                                    {
                                        MessageBox.Show("Loss value on row {0} could not be parsed. Using null.", "Splice Editor", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                                    }
                                }

                                FiberSplice fiberSplice = new FiberSplice(new Range(aUnit, aUnit), new Range(bUnit, bUnit), loss, typeObj);
                                currentGrid[aUnit] = fiberSplice;
                                currentBStrands.Add(bUnit);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                isNotOkString = ex.Message;
            }


            // Check the ranges are within the feature's units
            List <int> checkToUnits   = new List <int>();
            List <int> checkFromUnits = new List <int>();

            // Anything that is in the current grid, we will see if it is available. But if it was deleted, we can ignore
            // checking its availabilty, because we are about to free it up. Also if it was original, we can ignore it,
            // since we are reprocessing it. Duplicates ON the grid have already been checked for.
            // NOTE: We can simplify this to just check original, since any deleted ones were in the original.
            foreach (FiberSplice checkSplice in currentGrid.Values)
            {
                int unit = checkSplice.BRange.Low;
                checkToUnits.Add(unit);
            }

            foreach (FiberSplice checkSplice in _original.Values)
            {
                int unit = checkSplice.BRange.Low;
                if (checkToUnits.Contains(unit))
                {
                    checkToUnits.Remove(unit);
                }
            }

            foreach (int fromUnit in currentGrid.Keys)
            {
                if (!_original.ContainsKey(fromUnit))
                {
                    checkFromUnits.Add(fromUnit);
                }
            }

            if (!SpliceAndConnectionUtils.AreRangesAvailable(checkFromUnits, cableA, cableB.IsOtherFromEnd))
            {
                isNotOkString = "Some A units are not in the available ranges for the A Cable.";
            }
            else if (!SpliceAndConnectionUtils.AreRangesAvailable(checkToUnits, cableB, cableB.IsThisFromEnd))
            {
                isNotOkString = "Some B units are not in the available ranges for the B Cable.";
            }

            if (0 < isNotOkString.Length)
            {
                string message = string.Format("{0}\nPlease correct this and try again.", isNotOkString);
                MessageBox.Show(message, "Splice Editor", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else
            {
                if (null != cableA && null != cableB)
                {
                    // For the deleted ones, if they were added back, don't delete them...
                    List <int> keys = new List <int>();
                    keys.AddRange(_deleted.Keys);
                    foreach (int key in keys)
                    {
                        if (currentGrid.ContainsKey(key))
                        {
                            FiberSplice fiberSplice = currentGrid[key];
                            if (fiberSplice.BRange.Low == _deleted[key].BRange.Low &&
                                fiberSplice.Loss == _deleted[key].Loss &&
                                fiberSplice.Type == _deleted[key].Type)
                            {
                                // It is added back, so don't delete it
                                _deleted.Remove(key);
                            }
                        }
                    }

                    if (0 < _deleted.Count)
                    {
                        _spliceHelper.BreakSplices(cableA, cableB, splice, _deleted, false);
                    }

                    // For the added ones, if they already exist or are not available, don't add them
                    // Since we already know they are in the fiber count range, the only problem would be if they were already
                    // spliced. This would be the case if (1) it was part of the original or (2) has already appeared higher
                    // on the currentGrid. (2) is handled when building currentGrid, by checking if the aUnit or bUnit was already used.
                    keys.Clear();
                    keys.AddRange(currentGrid.Keys);
                    foreach (int key in keys)
                    {
                        if (_original.ContainsKey(key))
                        {
                            FiberSplice fiberSplice = currentGrid[key];
                            if (fiberSplice.BRange.Low == _original[key].BRange.Low &&
                                fiberSplice.Loss == _original[key].Loss &&
                                fiberSplice.Type == _original[key].Type)
                            {
                                // It was on the original, so we don't need to create it
                                currentGrid.Remove(key);
                            }
                        }
                    }

                    if (0 < currentGrid.Count)
                    {
                        _spliceHelper.CreateSplices(cableA, cableB, splice, currentGrid, false);
                    }

                    // These are no longer part of the originals
                    foreach (KeyValuePair <int, FiberSplice> deletedPair in _deleted)
                    {
                        _original.Remove(deletedPair.Key);
                    }

                    // These are now part of the originals
                    foreach (KeyValuePair <int, FiberSplice> addedPair in currentGrid)
                    {
                        _original[addedPair.Key] = addedPair.Value;
                    }

                    _deleted.Clear(); // The grid is fresh

                    // Set the existing rows as committed data. Less than count-1 lets us avoid the insert row
                    for (int i = 0; i < grdSplices.Rows.Count - 1; i++)
                    {
                        grdSplices.Rows[i].ReadOnly = true;
                    }

                    btnSave.Enabled = false;
                    btnSave.Tag     = false; // No edits made yet
                    result          = true;
                }
            }

            return(result);
        }
        /// <summary>
        /// Check changes to the grid and save them to the database
        /// </summary>
        /// <param name="from">From feature</param>
        /// <param name="to">To feature</param>
        /// <returns>Success</returns>
        private bool SaveChanges(FeatureWrapper from, FeatureWrapper to)
        {
            bool   result        = false;
            string isNotOkString = string.Empty;

            Dictionary <int, int> currentGrid = new Dictionary <int, int>();
            FiberCableWrapper     cable       = null;
            DeviceWrapper         device      = null;
            bool     isFromEnd = false;
            PortType portType  = PortType.Input;

            #region Detect Direction
            try
            {
                if (from is FiberCableWrapper && to is ConnectableDeviceWrapper)
                {
                    cable     = cboFrom.SelectedItem as FiberCableWrapper;
                    device    = cboTo.SelectedItem as DeviceWrapper;
                    isFromEnd = ((ConnectableDeviceWrapper)device).IsCableFromEnd;
                    portType  = PortType.Input;
                }
                else if (from is DeviceWrapper && to is ConnectableCableWrapper)
                {
                    device    = cboFrom.SelectedItem as DeviceWrapper;
                    cable     = cboTo.SelectedItem as FiberCableWrapper;
                    isFromEnd = ((ConnectableCableWrapper)cable).IsThisFromEnd;
                    portType  = PortType.Output;
                }
                else
                {
                    isNotOkString = "Must connect a cable to a device, or device to a cable.";
                }
            }
            catch (Exception ex)
            {
                isNotOkString = ex.Message;
            }
            #endregion

            try
            {
                if (null != cable && null != device)
                {
                    // Only continue if we have a valid setup
                    try
                    {
                        int aIdx = colFromRange.Index;
                        int bIdx = colToRange.Index;

                        // Less than count-1 lets us avoid the insert row
                        for (int i = 0; i < grdConnections.Rows.Count - 1; i++)
                        {
                            object       aRanges    = (grdConnections[aIdx, i].Value != null ? grdConnections[aIdx, i].Value : "");
                            object       bRanges    = (grdConnections[bIdx, i].Value != null ? grdConnections[bIdx, i].Value : "");
                            List <Range> fromRanges = SpliceAndConnectionUtils.ParseRanges(aRanges.ToString());
                            List <Range> toRanges   = SpliceAndConnectionUtils.ParseRanges(bRanges.ToString());

                            // Check that counts match up
                            if (!SpliceAndConnectionUtils.AreCountsEqual(fromRanges, toRanges))
                            {
                                isNotOkString = "Number of units from A to B must match on each row.";
                            }

                            // Check the ranges are within the feature's units
                            if (PortType.Input == portType)
                            {
                                if (!SpliceAndConnectionUtils.AreRangesWithinFiberCount(fromRanges, cable))
                                {
                                    isNotOkString = "Selected units exceed fiber count for cable.";
                                }
                                else if (!SpliceAndConnectionUtils.AreRangesWithinPortCount(toRanges, device, portType))
                                {
                                    isNotOkString = "Selected units exceed input port count for device.";
                                }
                            }
                            else
                            {
                                if (!SpliceAndConnectionUtils.AreRangesWithinFiberCount(toRanges, cable))
                                {
                                    isNotOkString = "Selected units exceed fiber count for cable.";
                                }
                                else if (!SpliceAndConnectionUtils.AreRangesWithinPortCount(fromRanges, device, portType))
                                {
                                    isNotOkString = "Selected units exceed output port count for device.";
                                }
                            }

                            if (0 < isNotOkString.Length)
                            {
                                // No need to check the rest if this one was not OK
                                break;
                            }

                            List <Connection> matchedUp = SpliceAndConnectionUtils.MatchUp(fromRanges, toRanges);
                            foreach (Connection connection in matchedUp)
                            {
                                Range a        = connection.ARange;
                                Range b        = connection.BRange;
                                int   numUnits = a.High - a.Low + 1;
                                for (int offset = 0; offset < numUnits; offset++)
                                {
                                    int aUnit = a.Low + offset;
                                    int bUnit = b.Low + offset;

                                    if (currentGrid.ContainsKey(aUnit) ||
                                        currentGrid.ContainsValue(bUnit))
                                    {
                                        isNotOkString = string.Format("Duplicate connection found from {0} to {1}.", aUnit, bUnit);
                                        // No need to check the rest if this one was not OK
                                        break;
                                    }
                                    else
                                    {
                                        currentGrid[aUnit] = bUnit;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        isNotOkString = ex.Message;
                        MessageBox.Show(ex.Message);
                    }

                    // Check the ranges are within the feature's units
                    List <int> checkToUnits   = new List <int>();
                    List <int> checkFromUnits = new List <int>();

                    // Anything that is in the current grid, we will see if it is available. But if it was deleted, we can ignore
                    // checking its availabilty, because we are about to free it up. Also if it was original, we can ignore it,
                    // since we are reprocessing it. Duplicates ON the grid have already been checked for.
                    // NOTE: We can simplify this to just check original, since any deleted ones were in the original.
                    foreach (int toUnit in currentGrid.Values)
                    {
                        if (!_original.ContainsValue(toUnit))
                        {
                            checkToUnits.Add(toUnit);
                        }
                    }

                    foreach (int fromUnit in currentGrid.Keys)
                    {
                        if (!_original.ContainsKey(fromUnit))
                        {
                            checkFromUnits.Add(fromUnit);
                        }
                    }

                    if (PortType.Input == portType)
                    {
                        if (!SpliceAndConnectionUtils.AreRangesAvailable(checkToUnits, device, portType))
                        {
                            isNotOkString = "Some To units are not in the available ranges for the device.";
                        }
                        else if (!SpliceAndConnectionUtils.AreRangesAvailable(checkFromUnits, cable, isFromEnd))
                        {
                            isNotOkString = "Some From units are not in the available ranges for the cable.";
                        }
                    }
                    else
                    {
                        if (!SpliceAndConnectionUtils.AreRangesAvailable(checkFromUnits, device, portType))
                        {
                            isNotOkString = "Some From units are not in the available ranges for the device.";
                        }
                        else if (!SpliceAndConnectionUtils.AreRangesAvailable(checkToUnits, cable, isFromEnd))
                        {
                            isNotOkString = "Some To units are not in the available ranges for the cable.";
                        }
                    }

                    if (0 == isNotOkString.Length)
                    {
                        // For the deleted ones, if they were added back, don't delete them...
                        List <int> keys = new List <int>();
                        keys.AddRange(_deleted.Keys);
                        foreach (int key in keys)
                        {
                            if (currentGrid.ContainsKey(key) &&
                                currentGrid[key] == _deleted[key])
                            {
                                // It is added back, so don't delete it
                                _deleted.Remove(key);
                            }
                        }

                        _connectionHelper.BreakConnections(cable, device, _deleted, portType, false);

                        // For the added ones, if they already exist or are not available, don't add them
                        // Since we already know they are in the fiber count range, the only problem would be if they were already
                        // spliced. This would be the case if (1) it was part of the original, (2) has already appeared higher
                        // on the currentGrid, (3) is spliced to something else. (2) is handled when building currentGrid, by checking
                        // if the aUnit or bUnit was already used and (3) is checked in the AreRangesAvailable checks. So now we will
                        // confirm (1)...
                        keys.Clear();
                        keys.AddRange(currentGrid.Keys);
                        foreach (int key in keys)
                        {
                            if (_original.ContainsKey(key) &&
                                _original[key] == currentGrid[key])
                            {
                                currentGrid.Remove(key);
                            }
                        }

                        _connectionHelper.MakeConnections(cable, device, currentGrid, isFromEnd, portType, false);

                        // These are no longer part of the originals
                        foreach (int deletedKey in _deleted.Keys)
                        {
                            _original.Remove(deletedKey);
                        }

                        // These are now part of the originals
                        foreach (KeyValuePair <int, int> addedPair in currentGrid)
                        {
                            _original[addedPair.Key] = addedPair.Value;
                        }

                        _deleted.Clear(); // The grid is fresh

                        // Set the existing rows as committed data. Less than count-1 lets us avoid the insert row
                        for (int i = 0; i < grdConnections.Rows.Count - 1; i++)
                        {
                            grdConnections.Rows[i].ReadOnly = true;
                        }

                        btnSave.Enabled = false;
                        btnSave.Tag     = false; // No edits have been made
                        result          = true;
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error: " + ex.ToString());
            }

            if (0 < isNotOkString.Length)
            {
                string message = string.Format("{0}\nPlease correct this and try again.", isNotOkString);
                MessageBox.Show(message, "Connection Editor", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }

            return(result);
        }