use crate::heap_print::*;
pub use crate::machine::heap::*;
pub use crate::machine::machine_state::*;
pub use crate::machine::streams::*;
pub use crate::machine::*;
pub use crate::parser::ast::*;

#[cfg(test)]
use crate::machine::copier::CopierTarget;
use crate::read::TermWriteResult;

#[cfg(test)]
use std::ops::{Deref, DerefMut, Index, IndexMut, Range};

// a mini-WAM for test purposes.

pub struct MockWAM {
    pub machine_st: MachineState,
    pub op_dir: OpDir,
    //pub flags: MachineFlags,
}

#[allow(dead_code)]
impl MockWAM {
    pub fn new() -> Self {
        let op_dir = default_op_dir();

        Self {
            machine_st: MachineState::new(),
            op_dir,
        }
    }

    pub fn write_parsed_term_to_heap(
        &mut self,
        input_stream: Stream,
    ) -> Result<TermWriteResult, CompilationError> {
        self.machine_st.read(input_stream, &self.op_dir)
    }

    pub fn parse_and_write_parsed_term_to_heap(
        &mut self,
        term_string: &'static str,
    ) -> Result<TermWriteResult, CompilationError> {
        let stream = Stream::from_static_string(term_string, &mut self.machine_st.arena);
        self.write_parsed_term_to_heap(stream)
    }

    pub fn parse_and_print_term(
        &mut self,
        term_string: &'static str,
    ) -> Result<String, CompilationError> {
        let term_write_result = self.parse_and_write_parsed_term_to_heap(term_string)?;
        print_heap_terms(&self.machine_st.heap, term_write_result.heap_loc);

        let mut printer = HCPrinter::new(
            &mut self.machine_st.heap,
            &mut self.machine_st.stack,
            &self.machine_st.arena,
            &self.op_dir,
            PrinterOutputter::new(),
            term_write_result.heap_loc,
        );

        printer.var_names = term_write_result
            .var_dict
            .into_iter()
            .map(|(var, cell)| match var {
                VarKey::VarPtr(var) => (cell, var.clone()),
                VarKey::AnonVar(_) => (cell, VarPtr::from(var.to_string())),
            })
            .collect();

        Ok(printer.print().result())
    }
}

impl Default for MockWAM {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
pub struct TermCopyingMockWAM<'a> {
    pub wam: &'a mut MockWAM,
}

#[cfg(test)]
impl<'a> Index<usize> for TermCopyingMockWAM<'a> {
    type Output = HeapCellValue;

    #[inline]
    fn index(&self, index: usize) -> &HeapCellValue {
        &self.wam.machine_st.heap[index]
    }
}

#[cfg(test)]
impl<'a> IndexMut<usize> for TermCopyingMockWAM<'a> {
    #[inline]
    fn index_mut(&mut self, index: usize) -> &mut HeapCellValue {
        &mut self.wam.machine_st.heap[index]
    }
}

#[cfg(test)]
impl<'a> Deref for TermCopyingMockWAM<'a> {
    type Target = MockWAM;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.wam
    }
}

#[cfg(test)]
impl<'a> DerefMut for TermCopyingMockWAM<'a> {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.wam
    }
}

#[cfg(test)]
impl<'a> CopierTarget for TermCopyingMockWAM<'a> {
    fn store(&self, val: HeapCellValue) -> HeapCellValue {
        read_heap_cell!(val,
            (HeapCellValueTag::AttrVar | HeapCellValueTag::Var, h) => {
                self.wam.machine_st.heap[h]
            }
            (HeapCellValueTag::StackVar, s) => {
                self.wam.machine_st.stack[s]
            }
            _ => {
                val
            }
        )
    }

    fn deref(&self, mut val: HeapCellValue) -> HeapCellValue {
        loop {
            let value = self.store(val);

            if value.is_var() && value != val {
                val = value;
                continue;
            }

            return val;
        }
    }

    fn push_attr_var_queue(&mut self, attr_var_loc: usize) {
        self.wam
            .machine_st
            .attr_var_init
            .attr_var_queue
            .push(attr_var_loc);
    }

    fn stack(&mut self) -> &mut Stack {
        &mut self.wam.machine_st.stack
    }

    fn threshold(&self) -> usize {
        self.wam.machine_st.heap.cell_len()
    }

    #[inline(always)]
    fn copy_pstr_to_threshold(&mut self, pstr_loc: usize) -> Result<usize, AllocError> {
        self.wam.machine_st.heap.copy_pstr_within(pstr_loc)
    }

    #[inline(always)]
    fn as_slice_from<'b>(&'b self, from: usize) -> Box<dyn Iterator<Item = u8> + 'b> {
        Box::new(self.wam.machine_st.heap.as_slice()[from..].iter().cloned())
    }

    #[inline(always)]
    fn reserve(&mut self, num_cells: usize) -> Result<HeapWriter<'_>, AllocError> {
        self.wam.machine_st.heap.reserve(num_cells)
    }

    #[inline(always)]
    fn copy_slice_to_end(&mut self, bounds: Range<usize>) -> Result<(), AllocError> {
        self.wam.machine_st.heap.copy_slice_to_end(bounds)
    }
}

#[cfg(test)]
pub fn all_cells_marked_and_unforwarded(heap: &Heap, offset: usize) {
    for curr_idx in offset..heap.cell_len() {
        let cell = heap[curr_idx];

        assert!(
            cell.get_mark_bit(),
            "cell {cell:?} at index {curr_idx} is not marked"
        );
        assert!(
            !cell.get_forwarding_bit(),
            "cell {cell:?} at index {curr_idx} is forwarded"
        );
    }
}

#[cfg(test)]
pub fn unmark_all_cells(heap: &mut Heap, offset: usize) {
    for idx in offset..heap.cell_len() {
        heap[idx].set_mark_bit(false);
    }
}

#[cfg(test)]
pub fn all_cells_unmarked(iter: &impl SizedHeap) {
    let mut idx = 0;
    let cell_len = iter.cell_len();

    while idx < cell_len {
        let curr_idx = idx;
        idx += 1;
        let cell = iter[curr_idx];

        assert!(
            !cell.get_mark_bit(),
            "cell {cell:?} at index {curr_idx} is still marked"
        );
    }
}

#[cfg(test)]
pub(crate) fn write_parsed_term_to_heap(
    machine_st: &mut MachineState,
    input_stream: Stream,
    op_dir: &OpDir,
) -> Result<TermWriteResult, CompilationError> {
    machine_st.read(input_stream, op_dir)
}

#[cfg(test)]
pub(crate) fn parse_and_write_parsed_term_to_heap(
    machine_st: &mut MachineState,
    term_string: &'static str,
    op_dir: &OpDir,
) -> Result<TermWriteResult, CompilationError> {
    let stream = Stream::from_static_string(term_string, &mut machine_st.arena);
    write_parsed_term_to_heap(machine_st, stream, op_dir)
}

impl Machine {
    /// For use in tests.
    #[allow(clippy::unbuffered_bytes)]
    pub fn test_load_file(&mut self, file: &str) -> Vec<u8> {
        let stream = Stream::from_owned_string(
            std::fs::read_to_string(AsRef::<std::path::Path>::as_ref(file)).unwrap(),
            &mut self.machine_st.arena,
        );

        self.load_file(file, stream);
        self.user_output.bytes().map(|b| b.unwrap()).collect()
    }

    /// For use in tests.
    #[allow(clippy::unbuffered_bytes)]
    pub fn test_load_string(&mut self, code: &str) -> Vec<u8> {
        let stream = Stream::from_owned_string(code.to_owned(), &mut self.machine_st.arena);

        self.load_file("<stdin>", stream);
        self.user_output.bytes().map(|b| b.unwrap()).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::functor_macro::FunctorElement;

    #[test]
    fn unify_tests() {
        let mut wam = MachineState::new();
        let mut op_dir = default_op_dir();

        op_dir.insert((atom!("+"), Fixity::In), OpDesc::build_with(500, YFX));
        op_dir.insert((atom!("-"), Fixity::In), OpDesc::build_with(500, YFX));
        op_dir.insert((atom!("*"), Fixity::In), OpDesc::build_with(500, YFX));
        op_dir.insert((atom!("/"), Fixity::In), OpDesc::build_with(400, YFX));
        op_dir.insert((atom!("="), Fixity::In), OpDesc::build_with(700, XFX));

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(b,a).", &op_dir).unwrap();

            unify!(
                wam,
                str_loc_as_cell!(0),
                str_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.fail = false;
        wam.heap.clear();

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(b,b).", &op_dir).unwrap();

            unify!(
                wam,
                str_loc_as_cell!(1),
                heap_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(!wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.fail = false;
        wam.heap.clear();

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(f(A),Y).", &op_dir).unwrap();

            unify!(
                wam,
                heap_loc_as_cell!(0),
                heap_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(!wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.fail = false;
        wam.heap.clear();

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(f(A),Y).", &op_dir).unwrap();

            unify!(
                wam,
                heap_loc_as_cell!(0),
                heap_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(!wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.fail = false;
        wam.heap.clear();

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(f(A),A).", &op_dir).unwrap();

            unify!(
                wam,
                heap_loc_as_cell!(0),
                heap_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(!wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.fail = false;
        wam.heap.clear();

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(A,f(A)).", &op_dir).unwrap();

            all_cells_unmarked(&wam.heap);

            unify!(
                wam,
                heap_loc_as_cell!(0),
                heap_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(!wam.fail);
        }

        all_cells_unmarked(&wam.heap);

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_pstr("this is a string"); // 0

            let h = section.cell_len();
            assert_eq!(h, 3);

            section.push_cell(heap_loc_as_cell!(h)); // 3
            section.push_pstr("this is a string"); // 4

            let h = section.cell_len();
            assert_eq!(h + 1, 8);

            section.push_cell(pstr_loc_as_cell!(heap_index!(h + 1))); // 7
            section.push_pstr("this is a string"); // 8

            section.push_cell(pstr_loc_as_cell!(heap_index!(h + 1)));
        });

        unify!(wam, pstr_loc_as_cell!(0), pstr_loc_as_cell!(heap_index!(4)));

        assert!(!wam.fail);

        assert_eq!(
            wam.heap
                .slice_to_str(heap_index!(0), "this is a string".len()),
            "this is a string"
        );
        assert_eq!(wam.heap[3], pstr_loc_as_cell!(heap_index!(8)));
        assert_eq!(
            wam.heap
                .slice_to_str(heap_index!(4), "this is a string".len()),
            "this is a string"
        );
        assert_eq!(wam.heap[7], pstr_loc_as_cell!(heap_index!(8)));
        assert_eq!(
            wam.heap
                .slice_to_str(heap_index!(8), "this is a string".len()),
            "this is a string"
        );
        assert_eq!(wam.heap[11], pstr_loc_as_cell!(heap_index!(8)));

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(list_loc_as_cell!(1));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(3));
            section.push_cell(atom_as_cell!(atom!("b")));
            section.push_cell(heap_loc_as_cell!(0));

            section.push_cell(list_loc_as_cell!(6));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(8));
            section.push_cell(atom_as_cell!(atom!("b")));
            section.push_cell(heap_loc_as_cell!(5));
        });

        unify!(wam, heap_loc_as_cell!(0), heap_loc_as_cell!(5));

        assert!(!wam.fail);

        all_cells_unmarked(&wam.heap);

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(list_loc_as_cell!(1));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(3));
            section.push_cell(atom_as_cell!(atom!("b")));
            section.push_cell(heap_loc_as_cell!(0));

            section.push_cell(list_loc_as_cell!(6));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(8));
            section.push_cell(atom_as_cell!(atom!("c")));
            section.push_cell(heap_loc_as_cell!(5));
        });

        unify!(wam, heap_loc_as_cell!(0), heap_loc_as_cell!(5));

        assert!(wam.fail);

        wam.fail = false;
        all_cells_unmarked(&wam.heap);

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(list_loc_as_cell!(1));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(3));
            section.push_cell(atom_as_cell!(atom!("b")));
            section.push_cell(heap_loc_as_cell!(5));

            section.push_cell(list_loc_as_cell!(6));
            section.push_cell(atom_as_cell!(atom!("a")));
            section.push_cell(list_loc_as_cell!(8));
            section.push_cell(atom_as_cell!(atom!("b")));
            section.push_cell(heap_loc_as_cell!(0));
        });

        unify!(wam, heap_loc_as_cell!(0), heap_loc_as_cell!(5));

        assert!(!wam.fail);
        all_cells_unmarked(&wam.heap);
        wam.heap.clear();

        {
            let term_write_result_1 =
                parse_and_write_parsed_term_to_heap(&mut wam, "X = g(X,y).", &op_dir).unwrap();

            print_heap_terms(&wam.heap, term_write_result_1.heap_loc);

            unify!(wam, heap_loc_as_cell!(2), str_loc_as_cell!(4));

            assert_eq!(wam.heap[2], str_loc_as_cell!(4));
        }
    }

    #[test]
    fn test_unify_with_occurs_check() {
        let mut wam = MachineState::new();
        let mut op_dir = default_op_dir();

        op_dir.insert((atom!("+"), Fixity::In), OpDesc::build_with(500, YFX));
        op_dir.insert((atom!("-"), Fixity::In), OpDesc::build_with(500, YFX));
        op_dir.insert((atom!("*"), Fixity::In), OpDesc::build_with(400, YFX));
        op_dir.insert((atom!("/"), Fixity::In), OpDesc::build_with(400, YFX));

        {
            parse_and_write_parsed_term_to_heap(&mut wam, "f(X,X).", &op_dir).unwrap();

            let term_write_result_2 =
                parse_and_write_parsed_term_to_heap(&mut wam, "f(A,f(A)).", &op_dir).unwrap();

            all_cells_unmarked(&wam.heap);

            unify_with_occurs_check!(
                wam,
                str_loc_as_cell!(0),
                str_loc_as_cell!(term_write_result_2.heap_loc)
            );

            assert!(wam.fail);
        }
    }

    #[test]
    fn test_term_compare() {
        use std::cmp::Ordering;

        let mut wam = MachineState::new();

        // clear the heap of resource error data etc
        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(heap_loc_as_cell!(0));
            section.push_cell(heap_loc_as_cell!(1));
        });

        assert_eq!(
            compare_term_test!(wam, wam.heap[0], wam.heap[1]),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(wam, wam.heap[1], wam.heap[0]),
            Some(Ordering::Greater)
        );

        assert_eq!(
            compare_term_test!(wam, wam.heap[0], wam.heap[0]),
            Some(Ordering::Equal)
        );

        assert_eq!(
            compare_term_test!(wam, wam.heap[1], wam.heap[1]),
            Some(Ordering::Equal)
        );

        let cstr_cell = wam.heap.allocate_cstr("string").unwrap();

        assert_eq!(
            compare_term_test!(wam, atom_as_cell!(atom!("atom")), cstr_cell),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(
                wam,
                atom_as_cell!(atom!("atom")),
                atom_as_cell!(atom!("atom"))
            ),
            Some(Ordering::Equal)
        );

        assert_eq!(
            compare_term_test!(
                wam,
                atom_as_cell!(atom!("atom")),
                atom_as_cell!(atom!("aaa"))
            ),
            Some(Ordering::Greater)
        );

        assert_eq!(
            compare_term_test!(
                wam,
                fixnum_as_cell!(Fixnum::build_with(6)),
                heap_loc_as_cell!(1)
            ),
            Some(Ordering::Greater)
        );

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(atom_as_cell!(atom!("f"), 1));
            section.push_cell(heap_loc_as_cell!(1));
        });

        assert_eq!(
            compare_term_test!(wam, heap_loc_as_cell!(0), heap_loc_as_cell!(0)),
            Some(Ordering::Equal)
        );

        assert_eq!(
            compare_term_test!(wam, heap_loc_as_cell!(0), atom_as_cell!(atom!("a"))),
            Some(Ordering::Greater)
        );

        wam.heap.clear();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            // [1,2,3]
            section.push_cell(list_loc_as_cell!(1));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(1)));
            section.push_cell(list_loc_as_cell!(3));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(2)));
            section.push_cell(list_loc_as_cell!(5));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(3)));
            section.push_cell(empty_list_as_cell!());

            // [1,2]
            section.push_cell(list_loc_as_cell!(8));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(1)));
            section.push_cell(list_loc_as_cell!(10));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(2)));
            section.push_cell(empty_list_as_cell!());
        });

        assert_eq!(
            compare_term_test!(wam, heap_loc_as_cell!(7), heap_loc_as_cell!(7)),
            Some(Ordering::Equal)
        );

        assert_eq!(
            compare_term_test!(wam, heap_loc_as_cell!(0), heap_loc_as_cell!(7)),
            Some(Ordering::Greater)
        );

        assert_eq!(
            compare_term_test!(wam, empty_list_as_cell!(), heap_loc_as_cell!(7)),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(
                wam,
                empty_list_as_cell!(),
                fixnum_as_cell!(Fixnum::build_with(1))
            ),
            Some(Ordering::Greater)
        );

        let cstr_cell = wam.heap.allocate_cstr("string").unwrap();

        assert_eq!(
            compare_term_test!(wam, empty_list_as_cell!(), cstr_cell),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(wam, empty_list_as_cell!(), atom_as_cell!(atom!("atom"))),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(wam, atom_as_cell!(atom!("atom")), empty_list_as_cell!()),
            Some(Ordering::Greater)
        );

        let one_p_one = HeapCellValue::from(float_alloc!(1.1, &mut wam.arena));

        assert_eq!(
            compare_term_test!(wam, one_p_one, fixnum_as_cell!(Fixnum::build_with(1))),
            Some(Ordering::Less)
        );

        assert_eq!(
            compare_term_test!(wam, fixnum_as_cell!(Fixnum::build_with(1)), one_p_one),
            Some(Ordering::Greater)
        );
    }

    #[test]
    fn is_cyclic_term_tests() {
        let mut wam = MachineState::new();

        let mut writer = wam.heap.reserve(96).unwrap();

        writer.write_with(|section| {
            section.push_cell(atom_as_cell!(atom!("f")));
            section.push_cell(fixnum_as_cell!(Fixnum::build_with(555)));
            section.push_cell(heap_loc_as_cell!(0));
        });

        assert!(!wam.is_cyclic_term(0));
        assert!(!wam.is_cyclic_term(1));
        assert!(!wam.is_cyclic_term(2));

        all_cells_unmarked(&wam.heap);
        wam.heap.clear();

        let mut functor_writer = Heap::functor_writer(functor!(
            atom!("f"),
            [atom_as_cell((atom!("a"))), atom_as_cell((atom!("b")))]
        ));

        functor_writer(&mut wam.heap).unwrap();

        let h = wam.heap.cell_len();
        wam.heap.push_cell(str_loc_as_cell!(0)).unwrap();

        assert!(!wam.is_cyclic_term(h));

        all_cells_unmarked(&wam.heap);

        assert!(!wam.is_cyclic_term(1));

        all_cells_unmarked(&wam.heap);

        assert!(!wam.is_cyclic_term(2));

        all_cells_unmarked(&wam.heap);

        wam.heap[2] = str_loc_as_cell!(0);

        print_heap_terms(&wam.heap, 0);

        assert!(wam.is_cyclic_term(2));

        all_cells_unmarked(&wam.heap);

        wam.heap[2] = atom_as_cell!(atom!("b"));
        wam.heap[1] = str_loc_as_cell!(0);

        assert!(wam.is_cyclic_term(1));

        all_cells_unmarked(&wam.heap);

        wam.heap.clear();

        let h = wam.heap.cell_len();
        wam.heap.allocate_cstr("a string").unwrap();

        assert!(!wam.is_cyclic_term(h));
    }
}
