1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
//! Module for parsing and handling place notation

use crate::{Bell, Block, IncompatibleStages, Row, RowBuf, SameStageVec, Stage};
use itertools::Itertools;
use std::{
    fmt::{Debug, Display, Formatter},
    ops::Range,
};

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ParseError {
    PlaceOutOfStage { place: u8, stage: Stage },
    AmbiguousPlacesBetween { p: u8, q: u8 },
    DuplicatePlace(u8),
    OddStageCross(Stage),
    NoPlacesGiven,
}

impl Display for ParseError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::OddStageCross(stage) => {
                write!(
                    f,
                    "Cross notation isn't valid for odd stages (in this case {})",
                    stage
                )
            }
            ParseError::PlaceOutOfStage { place, stage } => {
                write!(
                    f,
                    "Place '{}' is out of stage {}",
                    Bell::from_index(*place),
                    stage
                )
            }
            ParseError::AmbiguousPlacesBetween { p, q } => write!(
                f,
                "Ambiguous gap of {} bells between places '{}' and '{}'.",
                q - p - 1,
                Bell::from_index(*p),
                Bell::from_index(*q)
            ),
            ParseError::NoPlacesGiven => {
                write!(f, "No places given.  Use 'x' or '-' for a cross.")
            }
            ParseError::DuplicatePlace(p) => {
                write!(f, "Place '{}' is duplicated", Bell::from_index(*p))
            }
        }
    }
}

/// A single piece of place notation on any [`Stage`].
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct PlaceNot {
    /// A **0-indexed** list of which places are made during this `PlaceNot`.  We maintain the
    /// following invariants:
    /// - The places are fully expanded and unambiguous.  This means that every place made is
    ///   explicitly stored, with no implicit or ambiguous places.
    ///
    ///   For example, suppose the [`Stage`] is [`MAJOR`](Stage::MAJOR).
    ///    - The place notation "4" has an implicit place made at lead and so would be stored as
    ///      `vec![0, 3]`.
    ///    - The place notation "146" has an implicit place made in 5ths, so would be
    ///      stored as `vec![0, 3, 4, 5]`.
    /// - The places are stored **in ascending order**.  So "4817" would be stored as
    ///   `vec![0, 3, 6, 7]`.
    ///
    /// Enforcing these invariants improves the speed and simplicity of permutation and equality
    /// tests at the cost of (slightly) slower parsing.
    places: Vec<u8>,
    /// The [`Stage`] that this `PlaceNot` is intended to be used for.
    stage: Stage,
}

impl PlaceNot {
    /// Parse a string, interpreting it as a single `PlaceNot` of a given [`Stage`].  Like
    /// [`RowBuf::parse_with_stage`], this ignores chars that don't correspond to valid [`Bell`]
    /// names, including `&`, `.`, `,` and `+` (even though they have reserved meanings in blocks
    /// of place notation).  This will expand implicit places (even between two written places) but
    /// will fail if there is any kind of ambiguity, returning a [`ParseError`] describing the
    /// problem.
    ///
    /// # Example
    /// ```
    /// use bellframe::{Stage, PlaceNot, place_not::ParseError};
    ///
    /// // Parsing a valid place notation is OK
    /// assert_eq!(PlaceNot::parse("14", Stage::MAJOR)?.to_string(), "14");
    /// // Ordering and rogue chars don't matter
    /// assert_eq!(PlaceNot::parse("  4|7~18", Stage::ROYAL)?.to_string(), "1478");
    /// // Implicit places will be expanded
    /// assert_eq!(PlaceNot::parse("467", Stage::MAXIMUS)?.to_string(), "14567T");
    ///
    /// // Parsing invalid or ambiguous PN is not OK, and warns you of the problem
    /// assert_eq!(
    ///     PlaceNot::parse("14T", Stage::MAJOR).unwrap_err().to_string(),
    ///     "Place 'T' is out of stage Major"
    /// );
    /// assert_eq!(
    ///     PlaceNot::parse("15", Stage::MAJOR).unwrap_err().to_string(),
    ///     "Ambiguous gap of 3 bells between places '1' and '5'."
    /// );
    /// # Ok::<(), ParseError>(())
    /// ```
    pub fn parse(s: &str, stage: Stage) -> Result<Self, ParseError> {
        // If the string is any one of the cross strings, then return CROSS
        if s.len() == 1 && s.chars().next().map(CharMeaning::from) == Some(CharMeaning::Cross) {
            return Self::cross(stage).ok_or(ParseError::OddStageCross(stage));
        }
        // Parse the string into bell indices, ignoring any invalid characters
        let mut parsed_places: Vec<u8> = s
            .chars()
            .filter_map(Bell::from_name)
            .map(Bell::index_u8)
            .collect();
        // Create a new `PlaceNot` with these places (or error)
        Self::from_slice(&mut parsed_places, stage)
    }

    /// Creates a new `PlaceNot` from a sorted slice of places, performing bounds checks and
    /// returning errors if necessary.
    pub fn from_slice(input_places: &mut [u8], stage: Stage) -> Result<Self, ParseError> {
        // Sort the places into ascending order (unstable sort doesn't matter for integers)
        input_places.sort_unstable();
        let lowest_place = *input_places.first().ok_or(ParseError::NoPlacesGiven)?;
        let highest_place = *input_places.last().unwrap();

        // Check if any of the bells are out of range
        if highest_place >= stage.num_bells_u8() {
            return Err(ParseError::PlaceOutOfStage {
                place: highest_place,
                stage,
            });
        }

        // Rebuild to a new Vec when adding places to avoid quadratic behaviour
        let mut places = Vec::with_capacity(input_places.len() + 5);
        // Add implicit place in lead
        if lowest_place % 2 == 1 {
            places.push(0);
        }
        // Copy the contents of `parsed_places`, inserting implicit places where necessary
        for (p, q) in input_places.iter().copied().tuple_windows() {
            // Add `p` to `places`
            places.push(p);
            // Check if there is an implicit place made between these, or if the place notation is
            // ambiguous
            let num_intermediate_places = (q - p)
                .checked_sub(1) // If p == q, then `p - q == 0` and this subtraction will underflow ...
                .ok_or(ParseError::DuplicatePlace(p))?; // ... so report the duplicate places
            if num_intermediate_places == 1 {
                places.push(p + 1);
            } else if num_intermediate_places % 2 == 1 {
                // Any other even number causes an error
                return Err(ParseError::AmbiguousPlacesBetween { p, q });
            }
            // `q` will be pushed in the next loop iteration
        }
        // Copy the last element from `places` (aka the highest place).  This is a special case,
        // because `tuple_windows` won't return the last element as the first element of a tuple
        // window (because there's nothing to pair it with)
        places.push(highest_place);
        // Add implicit place at the back if necessary
        if (stage.num_bells_u8() - highest_place) % 2 == 0 {
            places.push(stage.num_bells_u8() - 1);
        }

        // Create struct and return.  We don't need to sort `places`, because we only pushed to it
        // in ascending order.
        Ok(PlaceNot { places, stage })
    }

    /// Returns a new `PlaceNot` representing the 'cross' notation on a given stage.  This will
    /// fail if `stage` doesn't have an even number of bells.
    ///
    /// # Example
    /// ```
    /// use bellframe::{PlaceNot, Stage};
    ///
    /// // These are crosses
    /// assert_eq!(
    ///     PlaceNot::cross(Stage::MAJOR).unwrap(),
    ///     PlaceNot::parse("x", Stage::MAJOR)?
    /// );
    /// # Ok::<(), bellframe::place_not::ParseError>(())
    /// ```
    pub fn cross(stage: Stage) -> Option<Self> {
        if stage.num_bells() % 2 == 0 {
            Some(PlaceNot {
                places: Vec::new(),
                stage,
            })
        } else {
            None
        }
    }

    /// Checks if this `PlaceNot` corresponds to the 'cross' notation.
    ///
    /// # Example
    /// ```
    /// use bellframe::{PlaceNot, Stage};
    ///
    /// // These are crosses
    /// assert!(PlaceNot::cross(Stage::MAJOR).unwrap().is_cross());
    /// assert!(PlaceNot::parse("x", Stage::MAJOR)?.is_cross());
    /// // These are not
    /// assert!(!PlaceNot::parse("14", Stage::MAJOR)?.is_cross());
    /// assert!(!PlaceNot::parse("3", Stage::TRIPLES)?.is_cross());
    /// # Ok::<(), bellframe::place_not::ParseError>(())
    /// ```
    #[inline(always)]
    pub fn is_cross(&self) -> bool {
        self.places.is_empty()
    }

    /// Checks whether a given `place` is made in this `PlaceNot`.
    #[inline(always)]
    pub fn contains(&self, place: u8) -> bool {
        self.places.contains(&place)
    }

    /// Checks whether internal places are made in this `PlaceNot`
    #[inline(always)]
    pub fn has_internal_places(&self) -> bool {
        let has_lead = self.places.first() == Some(&0);
        let has_lie = self.places.last() == Some(&(self.stage.num_bells_u8() - 1));
        let num_external_places = usize::from(has_lead) + usize::from(has_lie);
        self.places.len() > num_external_places
    }

    /// Returns the [`Stage`] of this `PlaceNot`
    #[inline(always)]
    pub fn stage(&self) -> Stage {
        self.stage
    }

    /// Returns the [`PlaceNot`] that goes between two [`Row`]s, or `None` if the [`Row`]s are not
    /// adjacent.
    pub fn pn_between(r1: &Row, r2: &Row) -> Option<PlaceNot> {
        if r1.stage() != r2.stage() {
            return None;
        }

        let mut places = Vec::<u8>::new();
        let mut bell_pair_iter = r1.bell_iter().zip_eq(r2.bell_iter()).enumerate();
        while let Some((place, (b1, b2))) = bell_pair_iter.next() {
            // If b1 and b2 are the same, then a place must be made
            if b1 == b2 {
                places.push(place as u8);
            } else {
                // Otherwise, we expect b1 and b2 to swap round in the next place:
                // ... b1  b2 ...
                //      (to)
                // ... b2  b1 ...
                if Some((place + 1, (b2, b1))) != bell_pair_iter.next() {
                    return None;
                }
            }
        }

        Some(Self {
            places,
            stage: r1.stage(),
        })
    }

    /// Returns a [`RowBuf`] representing the same transposition as this `PlaceNot`.
    pub fn transposition(&self) -> RowBuf {
        let mut row = RowBuf::rounds(self.stage());
        // SAFETY: `row` has the same stage as `self`
        unsafe { self.permute_unchecked(&mut row) };
        row
    }

    /// Uses this `PlaceNot` to perform an in-place permutation of a given [`Row`].  If you want to
    /// to preserve the old [`Row`], then use [`permute_new`](Self::permute_new).
    pub fn permute(&self, row: &mut Row) -> Result<(), IncompatibleStages> {
        IncompatibleStages::test_err(row.stage(), self.stage)?;
        unsafe { self.permute_unchecked(row) };
        Ok(())
    }

    /// Uses this `PlaceNot` to perform an in-place permutation of a given [`Row`], **without**
    /// checking that the [`Stage`]s match.  If you want to to preserve the old [`Row`], then use
    /// [`permute_new_unchecked`](Self::permute_new).
    ///
    /// # Safety
    ///
    /// This is safe if `self.stage() == row.stage()`.
    pub unsafe fn permute_unchecked(&self, row: &mut Row) {
        let mut places = self.places.iter().copied().peekable();
        let mut i = 0;
        while i < self.stage.num_bells_u8() {
            if places.peek() == Some(&i) {
                // If this PN contains a place at this index, then the bell in this place stays
                // where it is, so no change is required.
                places.next();
                i += 1;
            } else {
                // If this isn't a place, then we know by invariant that i + 1 is also not a place
                // (or out of range), so we perform a swap and move on by two bells
                row.swap(i as usize, i as usize + 1);
                i += 2;
            }
        }
    }

    /// Uses this `PlaceNot` to permute a given [`Row`], preserving the old copy and returning a
    /// new [`Row`].  This checks that the [`Stage`]s are equal, and is therefore safe.
    pub fn permute_new(&self, row: &Row) -> Result<RowBuf, IncompatibleStages> {
        IncompatibleStages::test_err(row.stage(), self.stage)?;
        Ok(unsafe { self.permute_new_unchecked(row) })
    }

    /// Uses this `PlaceNot` to permute a given [`Row`], preserving the old copy and returning a
    /// new [`Row`].
    ///
    /// # Safety
    ///
    /// This function is safe to use only when `self.stage() == row.stage()`.
    pub unsafe fn permute_new_unchecked(&self, row: &Row) -> RowBuf {
        let mut new_row = row.to_owned();
        self.permute_unchecked(&mut new_row);
        new_row
    }
}

impl Debug for PlaceNot {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "PlaceNot({})", self)
    }
}

impl Display for PlaceNot {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.is_cross() {
            // Always display cross notation as '-' to avoid confusion with bell names
            write!(f, "-")
        } else {
            // Otherwise concatenate all the bell names together
            for p in &self.places {
                write!(f, "{}", Bell::from_index(*p))?;
            }
            Ok(())
        }
    }
}

/// The possible ways that parsing a block of place notations could fail
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PnBlockParseError {
    /// A '+' was found somewhere other than the start of a block (e.g. in `16x16+16,12`).  The
    /// argument refers to the byte index of the location of the '+' within the parse string.
    PlusNotAtBlockStart(usize),
    /// One of the pieces of place notation was invalid.  The [`Range`] points to the byte range
    /// within the input string where the invalid place notation string was found, whereas the
    /// [`ParseError`] describes the problem.
    PnError(Range<usize>, ParseError),
    /// The string represents a block with no place notations.  This would violate the invariants
    /// of [`PnBlock`], so is an error.
    EmptyBlock,
}

impl Display for PnBlockParseError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            PnBlockParseError::EmptyBlock => write!(f, "`PnBlock`s can't be empty."),
            PnBlockParseError::PnError(range, err) => {
                write!(f, "Error parsing PN at index {}: {}", range.start, err)
            }
            PnBlockParseError::PlusNotAtBlockStart(index) => {
                write!(f, "'+' at index {} is not at the start of a block.", index)
            }
        }
    }
}

impl std::error::Error for PnBlockParseError {}

/// The different ways a `PnBlock` could be found to be invalid
#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)]
pub enum InvalidPnBlockError {
    IncompatibleStages(usize, IncompatibleStages),
    EmptyBlock,
}

impl Display for InvalidPnBlockError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            InvalidPnBlockError::IncompatibleStages(ind, e) => write!(
                f,
                "Incompatible stages: First PN has stage {}, whereas the {}th has {}",
                e.lhs_stage, ind, e.rhs_stage
            ),
            InvalidPnBlockError::EmptyBlock => write!(f, "`PnBlock`s can't be empty."),
        }
    }
}

impl std::error::Error for InvalidPnBlockError {}

/// A contiguous block of [`PlaceNot`]s.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PnBlock {
    /// The underlying [`PlaceNot`]s that make up this block.  This has to satisfy the following
    /// invariants:
    /// - `pns` cannot be empty, since that would correspond to a zero-length [`Block`], which is
    ///   not allowed
    /// - All the [`PlaceNot`]s must have the same [`Stage`].
    pns: Vec<PlaceNot>,
}

// PnBlocks can't have zero length, so `is_empty` is unnecessary
#[allow(clippy::len_without_is_empty)]
impl PnBlock {
    /// Parse a string slice into a `PnBlock`, checking for ambiguity and correctness.  This also
    /// expands symmetric blocks and implicit places.
    pub fn parse(s: &str, stage: Stage) -> Result<Self, PnBlockParseError> {
        let address_of_start_of_s = s.as_ptr() as usize;
        let mut pns: Vec<PlaceNot> = Vec::new();
        // A re-usuable chunk of memory used to store the unexpanded version of a symblock before
        // copying it into `pns`.
        let mut sym_block_buf: Vec<PlaceNot> = Vec::new();
        let is_single_block = !s.contains(',');
        // Split `s` into symmetric blocks, which are delimited by `,`
        for sym_block in s.split(',') {
            // Calculate the index of the start of this block within `s`, so that errors can be
            // pinpointed accurately
            let byte_offset = sym_block.as_ptr() as usize - address_of_start_of_s;
            // Parse this symblock as an asymmetric block into `sym_block_buf`
            let is_asymmetric =
                Self::parse_asymmetric_block(sym_block, byte_offset, stage, &mut sym_block_buf)?;

            // Handle the output of parsing the current block
            if is_single_block || is_asymmetric {
                pns.append(&mut sym_block_buf);
            } else {
                // Clone sym_block_buf into `pns` in order
                pns.extend_from_slice(&sym_block_buf);
                // **Move** pns except the last one from sym_block_buf in reverse order
                pns.extend(sym_block_buf.drain(..).rev().skip(1));
            }
        }
        // Return an error if pns is empty, otherwise construct the block
        if pns.is_empty() {
            Err(PnBlockParseError::EmptyBlock)
        } else {
            Ok(PnBlock { pns })
        }
    }

    fn parse_asymmetric_block(
        block: &str,
        block_start_offset: usize,
        stage: Stage,
        buf: &mut Vec<PlaceNot>,
    ) -> Result<bool, PnBlockParseError> {
        // Check that the buffer is empty -- it should be, because this will only be used in
        // `Self::parse`
        debug_assert!(buf.is_empty());
        // Create an iterator over the chars in block that we will then read from left to right and
        // parse
        let mut tok_indices = block
            .char_indices()
            .map(|(i, c)| (i + block_start_offset, CharMeaning::from(c)))
            // Insert a 'fake' delimiter at the end, to make sure that the last chunk of place
            // notation is not ignored
            .chain(std::iter::once((
                block_start_offset + block.len(),
                CharMeaning::Delimiter,
            )))
            // We need one a lookahead of one char to make parsing easier
            .peekable();

        /* Step 1: Skip meaningless chars at the left of the string */
        loop {
            if let Some((_i, c)) = tok_indices.peek() {
                if matches!(c, CharMeaning::Delimiter | CharMeaning::Unknown) {
                    tok_indices.next();
                    continue;
                }
            }
            break;
        }

        /* Step 2: If the first non-delimiter char we see represents an asymmetric block, then
         * consume it and log the asymmetricness of the block */
        let is_asymmetric = matches!(tok_indices.peek(), Some((_i, CharMeaning::Asym)));
        if is_asymmetric {
            // Consume the asymmetric-block char, and any rogue delimiters after it
            tok_indices.next();
        }

        // A buffer used to accumulate the block of places that are currently being parsed
        let mut places: Vec<u8> = Vec::new();
        // Tracks the index of the first byte in the chunk of PN currently being read.  This is
        // used so that we can return a byte range in the case of an error
        let mut current_pn_start_index = 0;
        for (i, m) in tok_indices {
            match m {
                // If the char is a bell name, then add it to the places
                CharMeaning::Bell(b) => {
                    if places.is_empty() {
                        // If this was the first place of the pn chunk, then we store its index as
                        // the start of this pn block
                        current_pn_start_index = i;
                    }
                    places.push(b.index_u8());
                }
                // If it's a cross notation or a delimiter, then we create a new PlaceNot out of
                // the places we've collected so far and push it to `buf`
                CharMeaning::Cross | CharMeaning::Delimiter => {
                    if !places.is_empty() {
                        // Push the new place notation to the buffer
                        let new_pn = PlaceNot::from_slice(&mut places, stage).map_err(|e| {
                            PnBlockParseError::PnError(current_pn_start_index..i, e)
                        })?;
                        buf.push(new_pn);
                        places.clear();
                    }
                }
                // A '+' (for asymmetric block) not at the start of a block is an error
                CharMeaning::Asym => return Err(PnBlockParseError::PlusNotAtBlockStart(i)),
                // Unknown characters are ignored
                CharMeaning::Unknown => continue,
            }
            // Push a cross notation if we see it, making sure to any errors
            if m == CharMeaning::Cross {
                buf.push(
                    PlaceNot::cross(stage)
                        .ok_or(ParseError::OddStageCross(stage))
                        .map_err(|e| PnBlockParseError::PnError(i..i + 1, e))?,
                );
            }
        }

        Ok(is_asymmetric)
    }

    /// Creates a new `PnBlock` from an [`Iterator`] of [`PlaceNot`]s, checking that the resulting
    /// `PnBlock` is valid (i.e. all the stages match and the `PnBlock` contains at least one
    /// [`PlaceNot`]).
    // This function returns a `Result`, so we can't use the `FromIterator` trait.  Anyway, I don't
    // think this is too confusing because we won't implement `FromIterator` on `PnBlock` anyway.
    #[allow(clippy::should_implement_trait)]
    pub fn from_iter(
        iter: impl IntoIterator<Item = PlaceNot>,
    ) -> Result<Self, InvalidPnBlockError> {
        // PERF: We could possibly avoid allocating if `iter` turns out to be invalid
        Self::from_vec(iter.into_iter().collect())
    }

    /// Creates a new `PnBlock` from a [`Vec`] of [`PlaceNot`]s, checking that the resulting
    /// `PnBlock` is valid (i.e. all the stages match and the `PnBlock` contains at least one
    /// [`PlaceNot`]).
    pub fn from_vec(pns: Vec<PlaceNot>) -> Result<Self, InvalidPnBlockError> {
        // Get the first stage and check that the block isn't empty
        let first_stage = pns.first().ok_or(InvalidPnBlockError::EmptyBlock)?.stage;
        // Check that all of the stages match
        for (i, pn) in pns.iter().enumerate().skip(1) {
            IncompatibleStages::test_err(first_stage, pn.stage)
                .map_err(|e| InvalidPnBlockError::IncompatibleStages(i, e))?;
        }
        // If all these checks pass, create a new block
        Ok(unsafe { Self::from_vec_unchecked(pns) })
    }

    /// Creates a new `PnBlock` from a [`Vec`] of [`PlaceNot`]s, **without** checking that the resulting
    /// `PnBlock` is valid.
    ///
    /// # Safety
    ///
    /// This is only safe if:
    /// - All the [`Stage`]s of the [`PlaceNot`]s match
    /// - The [`Vec`] is non-empty
    pub unsafe fn from_vec_unchecked(pns: Vec<PlaceNot>) -> Self {
        PnBlock { pns }
    }

    /// Returns an iterator over the [`PlaceNot`]s contained in this `PnBlock`
    #[inline]
    pub fn place_nots(&self) -> std::slice::Iter<PlaceNot> {
        self.pns.iter()
    }

    /// The [`Stage`] of this `PnBlock`.
    #[inline]
    pub fn stage(&self) -> Stage {
        // This index cannot fail, because we maintain an invariant that `self.pns` always has at
        // least one element.
        self.pns[0].stage
    }

    /// The number of [`PlaceNot`]s in this `PnBlock`.  This is also the `len` of any [`Block`]
    /// generated by applying this `PnBlock` to some [`Row`].
    #[inline]
    pub fn len(&self) -> usize {
        self.pns.len()
    }

    /// Generates the [`Row`]s which follow from applying `self` to a given [`Row`].  The resulting
    /// [`SameStageVec`] has length one greater than that of `self`, because it starts with
    /// `start_row` and then adds one new [`Row`] per [`PlaceNot`] in `self`.
    pub fn to_rows(&self, start_row: RowBuf) -> Result<SameStageVec, IncompatibleStages> {
        IncompatibleStages::test_err(start_row.stage(), self.stage())?;

        let mut rows = SameStageVec::from_row_buf(start_row.clone());
        let mut current_row = start_row;
        for pn in &self.pns {
            // SAFETY:
            // - all PlaceNots in `self` have the same stage (by invariant)
            // - we return with an error if `start_row` has a different `stage` to this `PnBlock`
            // => `pn.stage() == current_row.stage()`
            unsafe { pn.permute_unchecked(&mut current_row) };
            rows.push(&current_row).unwrap(); // Unwrap is fine, because we've already checked that
                                              // the stages match
        }
        Ok(rows)
    }

    /// Generates the [`Block`] of this `PnBlock`, starting at some `start_row`.  All the
    /// annotations are generated by `A::default()`.
    pub fn to_block<A>(&self, start_row: RowBuf) -> Result<Block<A>, IncompatibleStages>
    where
        A: Default,
    {
        let rows = self.to_rows(start_row)?;
        Ok(Block::with_default_annots(rows))
    }

    /// Generates the [`Block`] of this `PnBlock`, starting at [rounds](RowBuf::rounds).  All
    /// the annotations are generated by `A::default()`.
    pub fn to_block_from_rounds<A>(&self) -> Block<A>
    where
        A: Default,
    {
        // Unwrapping here is fine, because we provide a `RowBuf` that has the same `Stage` as
        // `self`
        self.to_block(RowBuf::rounds(self.stage())).unwrap()
    }
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum CharMeaning {
    Bell(Bell),
    Delimiter,
    Cross,
    Asym,
    Unknown,
}

impl From<char> for CharMeaning {
    fn from(c: char) -> Self {
        if let Some(b) = Bell::from_name(c) {
            CharMeaning::Bell(b)
        } else {
            match c {
                '+' => CharMeaning::Asym,
                ' ' | '.' => CharMeaning::Delimiter,
                'x' | 'X' | '-' => CharMeaning::Cross,
                _ => CharMeaning::Unknown,
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{ParseError, PnBlockParseError};
    use crate::{Block, PlaceNot, PnBlock, RowBuf, Stage};

    #[test]
    fn parse_ok() {
        #[track_caller]
        fn check(inp_string: &str, stage: Stage, exp_places: Vec<u8>) {
            let pn = PlaceNot::parse(inp_string, stage).unwrap();
            assert_eq!(pn.stage, stage);
            assert_eq!(pn.places, exp_places);
        }

        // No implict places
        check("14", Stage::MAJOR, vec![0, 3]);
        check("1256", Stage::MINOR, vec![0, 1, 4, 5]);
        check("16", Stage::MAXIMUS, vec![0, 5]);
        check("127", Stage::TRIPLES, vec![0, 1, 6]);
        check("3", Stage::CATERS, vec![2]);
        // Implicit places in lead
        check("4", Stage::MINIMUS, vec![0, 3]);
        check("234", Stage::MINOR, vec![0, 1, 2, 3]);
        check("478", Stage::MAJOR, vec![0, 3, 6, 7]);
        check("470", Stage::ROYAL, vec![0, 3, 6, 9]);
        check("2", Stage::TWO, vec![0, 1]);
        // Implicit places in lie
        check("3", Stage::MAJOR, vec![2, 7]);
        check("12", Stage::DOUBLES, vec![0, 1, 4]);
        check("1", Stage::TWO, vec![0, 1]);
        check("14", Stage::CINQUES, vec![0, 3, 10]);
        // Implicit places between two other places
        check("146", Stage::MAJOR, vec![0, 3, 4, 5]);
        check("13", Stage::SINGLES, vec![0, 1, 2]);
        check("13", Stage::TRIPLES, vec![0, 1, 2]);
        check("135", Stage::TRIPLES, vec![0, 1, 2, 3, 4]);
        // Implicit places in multiple places
        check("23", Stage::MAJOR, vec![0, 1, 2, 7]);
        check("4", Stage::TRIPLES, vec![0, 3, 6]);
        check("45", Stage::MINOR, vec![0, 3, 4, 5]);
        check("46", Stage::CATERS, vec![0, 3, 4, 5, 8]);
        // Out of order places
        check("6152", Stage::MINOR, vec![0, 1, 4, 5]);
        check("342", Stage::MINOR, vec![0, 1, 2, 3]);
        check("32", Stage::MAJOR, vec![0, 1, 2, 7]);
        check("21", Stage::DOUBLES, vec![0, 1, 4]);
        // Misc characters
        check("2\t  1 |", Stage::DOUBLES, vec![0, 1, 4]);
        check("   6\n?15&2!", Stage::MINOR, vec![0, 1, 4, 5]);
    }

    #[test]
    fn parse_err_odd_bell_cross() {
        for num_bells in 1u8..=u8::MAX {
            let stage = Stage::new(num_bells);
            let exp_result = if num_bells % 2 == 0 {
                Ok(PlaceNot::cross(stage).unwrap()) // Even stages parse correctly
            } else {
                Err(ParseError::OddStageCross(stage)) // Odd stages cause an error
            };
            for cross_not in &["x", "X", "-"] {
                assert_eq!(PlaceNot::parse(*cross_not, stage), exp_result);
            }
        }
    }

    #[test]
    fn parse_err_place_out_of_stage() {
        #[track_caller]
        fn check(inp_string: &str, stage: Stage, place: u8) {
            assert_eq!(
                PlaceNot::parse(inp_string, stage),
                Err(ParseError::PlaceOutOfStage { stage, place })
            );
        }

        check("148", Stage::MINIMUS, 7);
        check("91562", Stage::MINOR, 8);
        check("  3", Stage::TWO, 2);
    }

    #[test]
    fn parse_err_no_places_given() {
        for num_bells in 1..u8::MAX {
            assert_eq!(
                PlaceNot::parse("", Stage::new(num_bells)),
                Err(ParseError::NoPlacesGiven)
            );
        }
    }

    #[test]
    fn parse_err_ambiguous_gap() {
        for &(inp_string, stage, exp_p, exp_q) in &[
            // No implict places
            ("15", Stage::MAJOR, 0, 4),
            ("39", Stage::ROYAL, 2, 8),
            ("1925", Stage::MAXIMUS, 4, 8),
            ("1026", Stage::ROYAL, 1, 5),
        ] {
            assert_eq!(
                PlaceNot::parse(inp_string, stage),
                Err(ParseError::AmbiguousPlacesBetween { p: exp_p, q: exp_q })
            );
        }
    }

    #[test]
    fn parse_block_ok() {
        #[track_caller]
        fn check(stage: Stage, s1: &str, s2: &str, exp_len: usize) {
            let b1 = PnBlock::parse(s1, stage).unwrap();
            let b2 = PnBlock::parse(s2, stage).unwrap();
            assert_eq!(b1, b2);
            assert_eq!(b1.len(), exp_len);
        }

        check(Stage::SINGLES, "1.3", "1   .  3", 2);
        check(Stage::MINIMUS, "-4-3-1-..2", "x14x34x14x12", 8);
        check(Stage::MINIMUS, "-4-3-1-..2", "-14x34-14x12", 8);
        check(Stage::MINIMUS, "x14x14,12", "-14-14-14-12", 8);
        check(Stage::TRIPLES, "2.3", "2,3", 2);
        check(Stage::MAJOR, "x1,1x,x1,1x,x1,2", "-18-18-18-18,12", 16);
        check(Stage::MAJOR, "+x4x1,", "x14x18", 4);
        check(Stage::MAXIMUS, "x4x1,", "x14x1Tx14x", 7);
        check(Stage::MAXIMUS, "xxx1", "---1T", 4);
        check(Stage::MAXIMUS, "x   -\tx1", "---1T", 4);
    }

    #[test]
    fn parse_block_err() {
        use PnBlockParseError as PE;

        #[track_caller]
        fn check(pn_str: &str, stage: Stage, exp_error: PnBlockParseError) {
            assert_eq!(PnBlock::parse(pn_str, stage), Err(exp_error));
        }

        check("", Stage::MAJOR, PE::EmptyBlock);
        check("  *!^\"£^%as=", Stage::MAJOR, PE::EmptyBlock);
        check("5+,3+46.5", Stage::MAJOR, PE::PlusNotAtBlockStart(1));
        check("+5,3+46.5", Stage::MAJOR, PE::PlusNotAtBlockStart(4));
        check("+5,+3456+5", Stage::MINOR, PE::PlusNotAtBlockStart(8));
        check(
            "x5x4.5x5.36.4x4.5x4x1,9",
            Stage::MAJOR,
            PE::PnError(
                22..23,
                ParseError::PlaceOutOfStage {
                    place: 8,
                    stage: Stage::MAJOR,
                },
            ),
        );
        check(
            "x5x4.5x5.13827.4x4.5x4x1,9",
            Stage::MAJOR,
            PE::PnError(9..14, ParseError::AmbiguousPlacesBetween { p: 2, q: 6 }),
        );
        check(
            "x5x4.5x5.1385271.4x4.5x4x1,9",
            Stage::MAJOR,
            PE::PnError(9..16, ParseError::DuplicatePlace(0)),
        );
    }

    #[test]
    fn pn_to_block() {
        let alnick_lead = "156342
516324
153642
513462
531426
354162
351426
534162
354612
345621
436512
463521
643251
634215
362451
326415
236145
321654
326145
231654
213645
123465
214356
124365
123456";
        let plain_bob_major_lead = "12345678
21436587
24163857
42618375
46281735
64827153
68472513
86745231
87654321
78563412
75836142
57381624
53718264
35172846
31527486
13254768
13527486";

        #[track_caller]
        fn check(stage: Stage, pn: &str, block_str: &str) {
            let expected_block = Block::parse(block_str).unwrap();
            let b1: Block<()> = PnBlock::parse(pn, stage)
                .unwrap()
                .to_block(expected_block.first_row().to_owned())
                .unwrap();
            assert_eq!(b1, expected_block);
        }

        check(Stage::MINOR, "34-36.14-12-36.14-14.36,12", alnick_lead); // Alnwick Surprise Minor
        check(Stage::MINOR, "34-3.4-2-3.4-4.3,+2", alnick_lead); // Alnwick Surprise Minor
        check(Stage::MAJOR, "x18x18x18x18,12", plain_bob_major_lead); // Plain Bob Major
    }

    #[test]
    fn pn_between_rows() {
        #[track_caller]
        fn check_ok(r1: &str, r2: &str, exp_pn: &str) {
            let row1 = RowBuf::parse(r1).unwrap();
            let row2 = RowBuf::parse(r2).unwrap();
            let exp_pn = PlaceNot::parse(exp_pn, row1.stage()).unwrap();
            assert_eq!(PlaceNot::pn_between(&row1, &row2), Some(exp_pn));
        }

        check_ok("12345678", "13245678", "145678");
        check_ok("12345678", "13246587", "14");
        check_ok("18247653", "81246753", "3478");
    }
}