use crate::atom_table::*;
use crate::parser::ast::*;

use crate::machine::heap::*;
use crate::machine::machine_errors::CycleSearchResult;
use crate::machine::system_calls::BrentAlgState;
use crate::types::*;

use std::cmp::Ordering;
use std::ops::Deref;
use std::str;

#[derive(Copy, Clone, Debug)]
pub struct PartialString(Atom);

fn scan_for_terminator<Iter: Iterator<Item = char>>(iter: Iter) -> usize {
    let mut terminator_idx = 0;

    for c in iter {
        if c == '\u{0}' && terminator_idx != 0 {
            return terminator_idx;
        }

        terminator_idx += c.len_utf8();
    }

    terminator_idx
}

impl From<Atom> for PartialString {
    #[inline]
    fn from(buf: Atom) -> PartialString {
        PartialString(buf)
    }
}

impl Into<Atom> for PartialString {
    #[inline]
    fn into(self: Self) -> Atom {
        self.0
    }
}

impl PartialString {
    #[inline]
    pub(super) fn new<'a>(src: &'a str, atom_tbl: &mut AtomTable) -> Option<(Self, &'a str)> {
        let terminator_idx = scan_for_terminator(src.chars());
        let pstr = PartialString(atom_tbl.build_with(&src[.. terminator_idx]));

        Some(if terminator_idx <  src.as_bytes().len() {
            (pstr, &src[terminator_idx + 1..])
        } else {
            (pstr, "")
        })
    }

    #[inline(always)]
    pub(crate) fn as_str_from(&self, n: usize) -> &str {
        &self.0.as_str()[n..]
    }
}

#[derive(Clone, Copy)]
pub struct HeapPStrIter<'a> {
    pub heap: &'a [HeapCellValue],
    pub focus: HeapCellValue,
    orig_focus: usize,
    brent_st: BrentAlgState,
    stepper: fn(&mut HeapPStrIter<'a>) -> Option<PStrIteratee>,
}

#[derive(Debug)]
pub struct PStrPrefixCmpResult {
    pub focus: usize,
    pub offset: usize,
    pub prefix_len: usize,
}

struct PStrIterStep {
    iteratee: PStrIteratee,
    next_hare: usize,
}

impl<'a> HeapPStrIter<'a> {
    pub fn new(heap: &'a [HeapCellValue], h: usize) -> Self {
        let value = heap[h];

        Self {
            heap,
            focus: value,
            orig_focus: h,
            brent_st: BrentAlgState::new(h),
            stepper: HeapPStrIter::pre_cycle_discovery_stepper,
        }
    }

    #[inline(always)]
    pub fn focus(&self) -> usize {
        self.brent_st.hare
    }

    #[inline(always)]
    pub fn at_string_terminator(&self) -> bool {
        self.focus.is_string_terminator(self.heap)
    }

    #[inline(always)]
    pub fn num_steps(&self) -> usize {
        self.brent_st.num_steps()
    }

    #[inline(always)]
    pub fn chars(mut self) -> PStrCharsIter<'a> {
        let item = self.next();
        PStrCharsIter { iter: self, item }
    }

    pub fn compare_pstr_to_string(&mut self, s: &str) -> Option<PStrPrefixCmpResult> {
        let mut result = PStrPrefixCmpResult {
            focus: self.brent_st.hare,
            offset: 0,
            prefix_len: 0,
        };

        let mut final_result = None;

        while let Some(PStrIterStep { iteratee, next_hare }) = self.step(self.brent_st.hare) {
            self.brent_st.hare = next_hare;
            self.focus = self.heap[iteratee.focus()];

            result.focus  = iteratee.focus();
            result.offset = iteratee.offset();

            match iteratee {
                PStrIteratee::Char(_, c1) => {
                    if let Some(c2) = s[result.prefix_len..].chars().next() {
                        if c1 != c2 {
                            return None;
                        } else {
                            result.prefix_len += c1.len_utf8();
                            result.offset += c1.len_utf8();
                        }
                    } else {
                        final_result = Some(result);
                        break;
                    }
                }
                PStrIteratee::PStrSegment(_, pstr_atom, n) => {
                    let pstr = PartialString::from(pstr_atom);
                    let t = pstr.as_str_from(n);
                    let s = &s[result.prefix_len..];

                    if s.len() >= t.len() {
                        if s.starts_with(t) {
                            result.prefix_len += t.len();
                            result.offset += t.len();
                        } else {
                            return None;
                        }
                    } else if t.starts_with(&s) {
                        result.prefix_len += s.len();
                        result.offset += s.len();

                        final_result = Some(result);
                        break;
                    } else {
                        return None;
                    }
                }
            }

            if s.len() == result.prefix_len {
                final_result = Some(result);
                break;
            }
        }

        if let Some(result) = &final_result {
            if self.at_string_terminator() {
                self.focus = empty_list_as_cell!();
                self.brent_st.hare = result.focus;
            } else {
                read_heap_cell!(self.heap[result.focus],
                    (HeapCellValueTag::Lis | HeapCellValueTag::Str | HeapCellValueTag::PStr) => {
                        self.focus = self.heap[self.brent_st.hare];
                    }
                    _ => {
                    }
                );
            }
        }

        final_result
    }

    fn walk_hare_to_cycle_end(&mut self) {
        // walk_hare_to_cycle_end assumes a cycle has been found,
        // so it is always safe to unwrap self.step()

        let orig_hare = self.brent_st.hare;

        self.brent_st.hare = self.orig_focus;
        self.brent_st.tortoise = self.orig_focus;

        for _ in 0 .. self.brent_st.lam {
            self.brent_st.hare = self.step(self.brent_st.hare).unwrap().next_hare;
        }

        while self.brent_st.hare != self.brent_st.tortoise {
            self.brent_st.tortoise = self.step(self.brent_st.tortoise).unwrap().next_hare;
            self.brent_st.hare = self.step(self.brent_st.hare).unwrap().next_hare;
        }

        self.focus = self.heap[orig_hare];
        self.brent_st.hare = orig_hare;
    }

    pub fn to_string(&mut self) -> String {
        let mut buf = String::with_capacity(32);

        while let Some(iteratee) = self.next() {
            match iteratee {
                PStrIteratee::Char(_, c) => {
                    buf.push(c);
                }
                PStrIteratee::PStrSegment(_, pstr_atom, n) => {
                    let pstr = PartialString::from(pstr_atom);
                    buf += pstr.as_str_from(n);
                }
            }
        }

        buf
    }

    #[inline]
    pub fn is_continuable(&self) -> bool {
        let mut focus = self.focus;

        loop {
           read_heap_cell!(focus,
               (HeapCellValueTag::CStr | HeapCellValueTag::PStrLoc) => {
                   return true;
               }
               (HeapCellValueTag::Atom, (name, arity)) => { // TODO: use Str here?
                   return name == atom!(".") && arity == 2;
               }
               (HeapCellValueTag::Lis, h) => {
                   let value = self.heap[h];
                   let value = heap_bound_store(
                       self.heap,
                       heap_bound_deref(self.heap, value),
                   );

                   return read_heap_cell!(value,
                       (HeapCellValueTag::Atom, (name, arity)) => {
                           arity == 0 && name.as_char().is_some()
                       }
                       (HeapCellValueTag::Char) => {
                           true
                       }
                       _ => {
                           false
                       }
                   );
               }
               (HeapCellValueTag::AttrVar | HeapCellValueTag::Var, h) => {
                   if focus == self.heap[h] {
                       return false;
                   }

                   focus = self.heap[h];
               }
               _ => {
                   return false;
               }
           );
        }
    }

    #[inline(always)]
    pub fn cycle_detected(&self) -> bool {
        self.stepper as usize == HeapPStrIter::post_cycle_discovery_stepper as usize
    }

    fn step(&self, mut curr_hare: usize) -> Option<PStrIterStep> {
        loop {
            read_heap_cell!(self.heap[curr_hare],
                (HeapCellValueTag::CStr, cstr_atom) => {
                    return if self.focus == empty_list_as_cell!() {
                        None
                    } else {
                        Some(PStrIterStep {
                            iteratee: PStrIteratee::PStrSegment(curr_hare, cstr_atom, 0),
                            next_hare: curr_hare,
                        })
                    }
                }
                (HeapCellValueTag::PStrLoc, h) => {
                    curr_hare = h;
                }
                (HeapCellValueTag::PStr, pstr_atom) => {
                    return Some(PStrIterStep {
                        iteratee: PStrIteratee::PStrSegment(curr_hare, pstr_atom, 0),
                        next_hare: curr_hare+1,
                    });
                }
                (HeapCellValueTag::PStrOffset, pstr_offset) => {
                    if self.focus == empty_list_as_cell!() {
                        return None;
                    }

                    let pstr_atom = cell_as_atom!(self.heap[pstr_offset]);
                    let n = cell_as_fixnum!(self.heap[curr_hare+1]).get_num() as usize;

                    return if self.heap[pstr_offset].get_tag() == HeapCellValueTag::CStr {
                        Some(PStrIterStep {
                            iteratee: PStrIteratee::PStrSegment(curr_hare, pstr_atom, n),
                            next_hare: pstr_offset,
                        })
                    } else {
                        Some(PStrIterStep {
                            iteratee: PStrIteratee::PStrSegment(curr_hare, pstr_atom, n),
                            next_hare: pstr_offset+1,
                        })
                    };
                }
                (HeapCellValueTag::Lis, h) => {
                    let value = heap_bound_store(
                        self.heap,
                        heap_bound_deref(self.heap, self.heap[h]),
                    );

                    return if let Some(c) = value.as_char() {
                        Some(PStrIterStep {
                            iteratee: PStrIteratee::Char(curr_hare, c),
                            next_hare: h+1,
                        })
                    } else {
                        None
                    };
                }
                (HeapCellValueTag::Str, s) => {
                    let (name, arity) = cell_as_atom_cell!(self.heap[s])
                        .get_name_and_arity();

                    return if name == atom!(".") && arity == 2 {
                        let value = heap_bound_store(
                            self.heap,
                            heap_bound_deref(self.heap, self.heap[s+1]),
                        );

                        if let Some(c) = value.as_char() {
                            Some(PStrIterStep {
                                iteratee: PStrIteratee::Char(curr_hare, c),
                                next_hare: s+2,
                            })
                        } else {
                            None
                        }
                    } else {
                        None
                    };
                }
                (HeapCellValueTag::Atom, (_name, arity)) => {
                    debug_assert!(arity == 0);
                    return None;
                }
                (HeapCellValueTag::AttrVar | HeapCellValueTag::Var, h) => {
                    if h == curr_hare {
                        return None;
                    }

                    curr_hare = h;
                }
                _ => {
                    return None;
                }
            );
        }
    }

    fn pre_cycle_discovery_stepper(&mut self) -> Option<PStrIteratee> {
        let PStrIterStep { iteratee, next_hare } =
            match self.step(self.brent_st.hare) {
                Some(results) => results,
                None => {
                    return None;
                }
            };

        self.focus = self.heap[iteratee.focus()];

        if self.at_string_terminator() {
            self.focus = empty_list_as_cell!();
            self.brent_st.hare = iteratee.focus();

            return Some(iteratee);
        }

        match self.brent_st.step(next_hare) {
            Some(cycle_result) => {
                debug_assert!(match cycle_result {
                    CycleSearchResult::Cyclic(..) => true,
                    _ => false,
                });

                self.walk_hare_to_cycle_end();
                self.stepper = HeapPStrIter::post_cycle_discovery_stepper;
            }
            None => {
                self.focus = self.heap[next_hare];
            }
        }

        Some(iteratee)
    }

    fn post_cycle_discovery_stepper(&mut self) -> Option<PStrIteratee> {
        if self.brent_st.hare == self.brent_st.tortoise {
            return None;
        }

        let PStrIterStep { iteratee, next_hare } =
            match self.step(self.brent_st.hare) {
                Some(results) => results,
                None => {
                    return None;
                }
            };

        self.focus = self.heap[next_hare];
        self.brent_st.hare = next_hare;

        Some(iteratee)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PStrIteratee {
    Char(usize, char),
    PStrSegment(usize, Atom, usize),
}

impl PStrIteratee {
    #[inline]
    fn offset(&self) -> usize {
        match self {
            PStrIteratee::Char(_, _) => 0,
            PStrIteratee::PStrSegment(_, _, n) => *n,
        }
    }

    #[inline]
    fn focus(&self) -> usize {
        match self {
            PStrIteratee::Char(focus, _) => *focus,
            PStrIteratee::PStrSegment(focus, _, _) => *focus,
        }
    }
}

impl<'a> Iterator for HeapPStrIter<'a> {
    type Item = PStrIteratee;

    #[inline(always)]
    fn next(&mut self) -> Option<Self::Item> {
        (self.stepper)(self)
    }
}

pub struct PStrCharsIter<'a> {
    pub iter: HeapPStrIter<'a>,
    pub item: Option<PStrIteratee>,
}

impl<'a> PStrCharsIter<'a> {
    pub fn peek(&self) -> Option<char> {
        if let Some(iteratee) = self.item {
            match iteratee {
                PStrIteratee::Char(_, c) => {
                    return Some(c);
                }
                PStrIteratee::PStrSegment(_, pstr_atom, n) => {
                    let pstr = PartialString::from(pstr_atom);
                    return pstr.as_str_from(n).chars().next();
                }
            }
        }

        None
    }
}

impl<'a> Deref for PStrCharsIter<'a> {
    type Target = HeapPStrIter<'a>;

    fn deref(&self) -> &Self::Target {
        &self.iter
    }
}

impl<'a> Iterator for PStrCharsIter<'a> {
    type Item = char;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some(item) = self.item {
            match item {
                PStrIteratee::Char(_, c) => {
                    self.item = self.iter.next();
                    return Some(c);
                }
                PStrIteratee::PStrSegment(f1, pstr_atom, n) => {
                    let pstr = PartialString::from(pstr_atom);

                    match pstr.as_str_from(n).chars().next() {
                        Some(c) => {
                            self.item = Some(PStrIteratee::PStrSegment(
                                f1,
                                pstr_atom,
                                n + c.len_utf8(),
                            ));

                            return Some(c);
                        }
                        None => {
                            self.item = self.iter.next();
                        }
                    }
                }
            }
        }

        None
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PStrCmpResult {
    Ordered(Ordering),
    FirstIterContinuable(PStrIteratee),
    SecondIterContinuable(PStrIteratee),
    Unordered,
}

impl PStrCmpResult {
    #[inline]
    pub fn is_second_iter(&self) -> bool {
        if let PStrCmpResult::SecondIterContinuable(_) = self {
            true
        } else {
            false
        }
    }
}

#[inline]
pub fn compare_pstr_prefixes<'a>(
    i1: &mut HeapPStrIter<'a>,
    i2: &mut HeapPStrIter<'a>,
) -> PStrCmpResult {
    #[inline(always)]
    fn step(iter: &mut HeapPStrIter, hare: usize) -> Option<PStrIterStep> {
        let result = iter.step(hare);
        iter.focus = iter.heap[hare];

        if iter.focus.is_string_terminator(iter.heap) {
            iter.focus = empty_list_as_cell!();
        }

        result
    }

    #[inline(always)]
    fn cycle_detection_step(i1: &mut HeapPStrIter, i2: &HeapPStrIter, step: &PStrIterStep) -> bool {
        if i1.cycle_detected() {
            i1.brent_st.hare = step.next_hare;
            i2.cycle_detected()
        } else if i1.brent_st.step(step.next_hare).is_some() {
            i1.stepper = HeapPStrIter::post_cycle_discovery_stepper;
            i2.cycle_detected()
        } else {
            false
        }
    }

    let mut r1 = step(i1, i1.brent_st.hare);
    let mut r2 = step(i2, i2.brent_st.hare);

    loop {
        if let Some(step_1) = r1.as_mut() {
            if let Some(step_2) = r2.as_mut() {
                match (step_1.iteratee, step_2.iteratee) {
                    (PStrIteratee::Char(_, c1), PStrIteratee::Char(_, c2)) => {
                        if c1 != c2 {
                            return PStrCmpResult::Ordered(c1.cmp(&c2));
                        }

                        cycle_detection_step(i1, i2, &step_1);
                        let both_cyclic = cycle_detection_step(i2, i1, &step_2);

                        r1 = step(i1, i1.brent_st.hare);
                        r2 = step(i2, i2.brent_st.hare);

                        if !both_cyclic {
                            continue;
                        }
                    }
                    (PStrIteratee::Char(_, c1), PStrIteratee::PStrSegment(f2, pstr_atom, n)) => {
                        let pstr = PartialString::from(pstr_atom);

                        if let Some(c2) = pstr.as_str_from(n).chars().next() {
                            if c1 != c2 {
                                return PStrCmpResult::Ordered(c1.cmp(&c2));
                            }

                            let n1 = n + c2.len_utf8();

                            if n1 < pstr_atom.len() {
                                step_2.iteratee = PStrIteratee::PStrSegment(f2, pstr_atom, n1);

                                let c1_result = cycle_detection_step(i1, i2, &step_1);
                                r1 = step(i1, i1.brent_st.hare);

                                if !c1_result {
                                    continue;
                                }
                            } else {
                                cycle_detection_step(i1, i2, &step_1);
                                let both_cyclic = cycle_detection_step(i2, i1, &step_2);

                                r1 = step(i1, i1.brent_st.hare);
                                r2 = step(i2, i2.brent_st.hare);

                                if !both_cyclic {
                                    continue;
                                }
                            }
                        } else {
                            let c2_result = cycle_detection_step(i2, i1, &step_2);
                            r2 = step(i2, i2.brent_st.hare);

                            if !c2_result {
                                continue;
                            }
                        }
                    }
                    (PStrIteratee::PStrSegment(f1, pstr_atom, n), PStrIteratee::Char(_, c2)) => {
                        let pstr = PartialString::from(pstr_atom);

                        if let Some(c1) = pstr.as_str_from(n).chars().next() {
                            if c1 != c2 {
                                return PStrCmpResult::Ordered(c1.cmp(&c2));
                            }

                            let n1 = n + c1.len_utf8();

                            if n1 < pstr_atom.len() {
                                step_1.iteratee = PStrIteratee::PStrSegment(f1, pstr_atom, n1);

                                let c2_result = cycle_detection_step(i2, i1, &step_2);
                                r2 = step(i2, step_2.next_hare);

                                if !c2_result {
                                    continue;
                                }
                            } else {
                                cycle_detection_step(i1, i2, &step_1);
                                let both_cyclic = cycle_detection_step(i2, i1, &step_2);

                                r1 = step(i1, i1.brent_st.hare);
                                r2 = step(i2, i2.brent_st.hare);

                                if !both_cyclic {
                                    continue;
                                }
                            }
                        } else {
                            let c1_result = cycle_detection_step(i1, i2, &step_1);
                            r1 = step(i1, i1.brent_st.hare);

                            if !c1_result {
                                continue;
                            }
                        }
                    }
                    (PStrIteratee::PStrSegment(f1, pstr1_atom, n1),
                     PStrIteratee::PStrSegment(f2, pstr2_atom, n2)) => {
                        if pstr1_atom == pstr2_atom && n1 == n2 {
                            cycle_detection_step(i1, i2, &step_1);
                            let both_cyclic = cycle_detection_step(i2, i1, &step_2);

                            r1 = step(i1, i1.brent_st.hare);
                            r2 = step(i2, i2.brent_st.hare);

                            if !both_cyclic {
                                continue;
                            }

                            break;
                        }

                        let pstr1 = PartialString::from(pstr1_atom);
                        let pstr2 = PartialString::from(pstr2_atom);

                        let str1 = pstr1.as_str_from(n1);
                        let str2 = pstr2.as_str_from(n2);

                        match str1.len().cmp(&str2.len()) {
                            Ordering::Equal if str1 == str2 => {
                                cycle_detection_step(i1, i2, &step_1);
                                let both_cyclic = cycle_detection_step(i2, i1, &step_2);

                                r1 = step(i1, i1.brent_st.hare);
                                r2 = step(i2, i2.brent_st.hare);

                                if !both_cyclic {
                                    continue;
                                }
                            }
                            Ordering::Less if str2.starts_with(str1) => {
                                step_2.iteratee = PStrIteratee::PStrSegment(f2, pstr2_atom, n2 + str1.len());
                                let c1_result = cycle_detection_step(i1, i2, &step_1);
                                r1 = step(i1, i1.brent_st.hare);

                                if !c1_result {
                                    continue;
                                }
                            }
                            Ordering::Greater if str1.starts_with(str2) => {
                                step_1.iteratee = PStrIteratee::PStrSegment(f1, pstr1_atom, n1 + str2.len());
                                let c2_result = cycle_detection_step(i2, i1, &step_2);
                                r2 = step(i2, i2.brent_st.hare);

                                if !c2_result {
                                    continue;
                                }
                            }
                            _ => {
                                return PStrCmpResult::Ordered(str1.cmp(str2));
                            }
                        }
                    }
                }
            }
        }

        break;
    }

    // to have a cyclic term, the cell at i1.focus must be:
    //
    // 1) 'continuable' as a cell in a string traversal, and,
    // 2) matchable by compare_pstr_prefixes to the cell at i2.focus.
    //
    // If both cells are continuable they must have been encountered
    // and thus matched by the compare_pstr_prefixes loop previously,
    // so here it suffices to check if they are both continuable.

    let r1_at_end = r1.is_none();
    let r2_at_end = r2.is_none();

    if r1_at_end && r2_at_end {
        if i1.focus == i2.focus {
            PStrCmpResult::Ordered(Ordering::Equal)
        } else {
            PStrCmpResult::Unordered
        }
    } else if r1_at_end {
        if i1.focus == empty_list_as_cell!() {
            PStrCmpResult::Ordered(Ordering::Less)
        } else {
            PStrCmpResult::SecondIterContinuable(r2.unwrap().iteratee)
        }
    } else if r2_at_end {
        if i2.focus == empty_list_as_cell!() {
            PStrCmpResult::Ordered(Ordering::Greater)
        } else {
            PStrCmpResult::FirstIterContinuable(r1.unwrap().iteratee)
        }
    } else {
        if i1.is_continuable() && i2.is_continuable() {
            PStrCmpResult::Ordered(Ordering::Equal)
        } else {
            PStrCmpResult::Unordered
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::machine::mock_wam::*;

    #[test]
    fn pstr_iter_tests() {
        let mut wam = MockWAM::new();

        let pstr_var_cell = put_partial_string(
            &mut wam.machine_st.heap,
            "abc ",
            &mut wam.machine_st.atom_tbl,
        );

        let pstr_cell = wam.machine_st.heap[pstr_var_cell.get_value() as usize];

        {
            let mut iter = HeapPStrIter::new(&wam.machine_st.heap, 0);

            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(0, cell_as_atom!(pstr_cell), 0))
            );
            assert_eq!(iter.next(), None);

            assert!(!iter.at_string_terminator());
        }

        wam.machine_st.heap.pop();
        wam.machine_st.heap.push(pstr_loc_as_cell!(2));

        let pstr_second_var_cell = put_partial_string(
            &mut wam.machine_st.heap,
            "def",
            &mut wam.machine_st.atom_tbl,
        );

        let pstr_second_cell = wam.machine_st.heap[pstr_second_var_cell.get_value() as usize];

        {
            let mut iter = HeapPStrIter::new(&wam.machine_st.heap, 0);

            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(0, cell_as_atom!(pstr_cell), 0))
            );
            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(2, cell_as_atom!(pstr_second_cell), 0))
            );

            assert_eq!(iter.next(), None);
            assert!(!iter.at_string_terminator());
        }

        wam.machine_st.heap.pop();
        wam.machine_st.heap.push(empty_list_as_cell!());

        {
            let mut iter = HeapPStrIter::new(&wam.machine_st.heap, 0);

            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(0, cell_as_atom!(pstr_cell), 0))
            );
            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(2, cell_as_atom!(pstr_second_cell), 0))
            );

            assert_eq!(iter.next(), None);
            assert!(iter.at_string_terminator());
        }

        wam.machine_st.heap.pop();
        wam.machine_st.heap.push(pstr_loc_as_cell!(wam.machine_st.heap.len() + 1));

        wam.machine_st.heap.push(pstr_offset_as_cell!(0));
        wam.machine_st.heap.push(fixnum_as_cell!(Fixnum::build_with(0)));

        {
            let mut iter = HeapPStrIter::new(&wam.machine_st.heap, 0);

            while let Some(_) = iter.next() {}

            assert!(!iter.at_string_terminator());
        }

        {
            let mut iter1 = HeapPStrIter::new(&wam.machine_st.heap, 0);
            let mut iter2 = HeapPStrIter::new(&wam.machine_st.heap, 0);

            assert_eq!(
                compare_pstr_prefixes(&mut iter1, &mut iter2),
                PStrCmpResult::Ordered(Ordering::Equal)
            );
        }

        {
            let second_h = wam.machine_st.heap.len();

            // construct a structurally similar but different cyclic partial string
            // matching the one beginning at wam.machine_st.heap[0].

            put_partial_string(
                &mut wam.machine_st.heap,
                "ab",
                &mut wam.machine_st.atom_tbl,
            );

            wam.machine_st.heap.pop();

            wam.machine_st.heap.push(pstr_loc_as_cell!(second_h+2));

            put_partial_string(
                &mut wam.machine_st.heap,
                "c ",
                &mut wam.machine_st.atom_tbl,
            );

            wam.machine_st.heap.pop();

            wam.machine_st.heap.push(pstr_loc_as_cell!(second_h+4));

            wam.machine_st.heap.push(pstr_second_cell);
            wam.machine_st.heap.push(pstr_loc_as_cell!(second_h+6));

            wam.machine_st.heap.push(pstr_offset_as_cell!(second_h));
            wam.machine_st.heap.push(fixnum_as_cell!(Fixnum::build_with(0)));

            let mut iter1 = HeapPStrIter::new(&wam.machine_st.heap, 0);
            let mut iter2 = HeapPStrIter::new(&wam.machine_st.heap, second_h);

            assert_eq!(
                compare_pstr_prefixes(&mut iter1, &mut iter2),
                PStrCmpResult::Ordered(Ordering::Equal)
            );
        }

        wam.machine_st.heap.clear();

        put_partial_string(
            &mut wam.machine_st.heap,
            "abc ",
            &mut wam.machine_st.atom_tbl,
        );

        let pstr_cell = wam.machine_st.heap[0];

        wam.machine_st.heap[1] = list_loc_as_cell!(2);

        wam.machine_st.heap.push(char_as_cell!('a'));
        wam.machine_st.heap.push(list_loc_as_cell!(4));
        wam.machine_st.heap.push(char_as_cell!('b'));
        wam.machine_st.heap.push(empty_list_as_cell!());

        wam.machine_st.heap.push(pstr_cell);
        wam.machine_st.heap.push(heap_loc_as_cell!(7));

        {
            let mut iter1 = HeapPStrIter::new(&wam.machine_st.heap, 0);
            let mut iter2 = HeapPStrIter::new(&wam.machine_st.heap, 6);

            assert_eq!(
                compare_pstr_prefixes(&mut iter1, &mut iter2),
                PStrCmpResult::FirstIterContinuable(PStrIteratee::Char(1, 'a')),
            );

            assert_eq!(iter2.focus, heap_loc_as_cell!(7));
        }

        // test "abc" = [X,Y,Z].

        wam.machine_st.heap.clear();

        let cstr_var_cell = put_complete_string(
            &mut wam.machine_st.heap,
            "abc",
            &mut wam.machine_st.atom_tbl,
        );

        wam.machine_st.heap.push(list_loc_as_cell!(2));
        wam.machine_st.heap.push(heap_loc_as_cell!(2));

        wam.machine_st.heap.push(list_loc_as_cell!(4));
        wam.machine_st.heap.push(heap_loc_as_cell!(4));

        wam.machine_st.heap.push(list_loc_as_cell!(6));
        wam.machine_st.heap.push(heap_loc_as_cell!(6));

        wam.machine_st.heap.push(empty_list_as_cell!());

        unify!(wam.machine_st, cstr_var_cell, heap_loc_as_cell!(1));

        assert_eq!(
            wam.machine_st.heap[2],
            char_as_cell!('a'),
        );

        assert_eq!(
            wam.machine_st.heap[4],
            char_as_cell!('b'),
        );

        assert_eq!(
            wam.machine_st.heap[6],
            char_as_cell!('c'),
        );

        // test "abc" = [X,Y,Z|D].

        wam.machine_st.heap.clear();

        let cstr_var_cell = put_complete_string(
            &mut wam.machine_st.heap,
            "abc",
            &mut wam.machine_st.atom_tbl,
        );

        wam.machine_st.heap.push(list_loc_as_cell!(2));
        wam.machine_st.heap.push(heap_loc_as_cell!(2)); // X

        wam.machine_st.heap.push(list_loc_as_cell!(4));
        wam.machine_st.heap.push(heap_loc_as_cell!(4)); // Y

        wam.machine_st.heap.push(list_loc_as_cell!(6));
        wam.machine_st.heap.push(heap_loc_as_cell!(6)); // Z

        wam.machine_st.heap.push(heap_loc_as_cell!(7)); // D

        unify!(wam.machine_st, cstr_var_cell, heap_loc_as_cell!(1));

        assert_eq!(wam.machine_st.fail, false);

        assert_eq!(
            wam.machine_st.heap[2],
            char_as_cell!('a'),
        );

        assert_eq!(
            wam.machine_st.heap[4],
            char_as_cell!('b'),
        );

        assert_eq!(
            wam.machine_st.heap[6],
            char_as_cell!('c'),
        );

        assert_eq!(
            wam.machine_st.heap[7],
            empty_list_as_cell!(),
        );

        // test "d" = [d].

        wam.machine_st.heap.clear();

        let cstr_var_cell = put_complete_string(
            &mut wam.machine_st.heap,
            "d",
            &mut wam.machine_st.atom_tbl,
        );

        wam.machine_st.heap.push(list_loc_as_cell!(2));
        wam.machine_st.heap.push(char_as_cell!('d'));
        wam.machine_st.heap.push(empty_list_as_cell!());

        unify!(wam.machine_st, cstr_var_cell, heap_loc_as_cell!(1));

        assert_eq!(wam.machine_st.fail, false);

        // test "abc" = [X,b,Z].

        wam.machine_st.heap.clear();

        let cstr_var_cell = put_complete_string(
            &mut wam.machine_st.heap,
            "abc",
            &mut wam.machine_st.atom_tbl,
        );

        wam.machine_st.heap.push(list_loc_as_cell!(2));
        wam.machine_st.heap.push(heap_loc_as_cell!(2));

        wam.machine_st.heap.push(list_loc_as_cell!(4));
        wam.machine_st.heap.push(char_as_cell!('b'));

        wam.machine_st.heap.push(list_loc_as_cell!(6));
        wam.machine_st.heap.push(heap_loc_as_cell!(6));

        wam.machine_st.heap.push(empty_list_as_cell!());

        unify!(wam.machine_st, cstr_var_cell, heap_loc_as_cell!(1));

        assert_eq!(wam.machine_st.fail, false);

        assert_eq!(
            wam.machine_st.heap[2],
            char_as_cell!('a'),
        );

        assert_eq!(
            wam.machine_st.heap[4],
            char_as_cell!('b'),
        );

        assert_eq!(
            wam.machine_st.heap[6],
            char_as_cell!('c'),
        );

        // test "abcdef" = [a,b,c|X].

        wam.machine_st.heap.clear();

        put_complete_string(
            &mut wam.machine_st.heap,
            "abcdef",
            &mut wam.machine_st.atom_tbl,
        );

        wam.machine_st.heap.push(pstr_as_cell!(atom!("abc")));
        wam.machine_st.heap.push(heap_loc_as_cell!(2));

        unify!(wam.machine_st, heap_loc_as_cell!(0), pstr_loc_as_cell!(1));

        print_heap_terms(wam.machine_st.heap.iter(), 0);

        assert_eq!(wam.machine_st.fail, false);

        assert_eq!(wam.machine_st.heap[2], pstr_loc_as_cell!(5));
        assert_eq!(wam.machine_st.heap[3], pstr_loc_as_cell!(1));
        assert_eq!(wam.machine_st.heap[4], atom_as_cstr_cell!(atom!("abcdef")));
        assert_eq!(wam.machine_st.heap[5], pstr_offset_as_cell!(4));
        assert_eq!(wam.machine_st.heap[6], fixnum_as_cell!(Fixnum::build_with("abc".len() as i64)));

        // test iteration on X = [b,c,b,c,b,c,b,c|...] as an offset.

        wam.machine_st.heap.clear();

        wam.machine_st.heap.push(pstr_as_cell!(atom!("abc")));
        wam.machine_st.heap.push(pstr_loc_as_cell!(2));
        wam.machine_st.heap.push(pstr_offset_as_cell!(0));
        wam.machine_st.heap.push(fixnum_as_cell!(Fixnum::build_with(1)));

        {
            let mut iter = HeapPStrIter::new(&wam.machine_st.heap, 2);

            assert_eq!(
                iter.next(),
                Some(PStrIteratee::PStrSegment(2, atom!("abc"), 1))
            );

            // assert!(iter.next().is_none());

            while let Some(_) = iter.next() {}
        }
    }
}
