//! The jointest module contains the logic for performing test evaluation.
//! Whenever two facts need to be compared, we say that they are joined if the tests for
//! this combination of facts succeeds.

use std::cmp::Ordering;
//use log::{debug, error, info, trace, warn};

use crate::condition::*;

/// A JoinTestField defines which field of a WME should be matched in a jointest.
///
/// JoinTests are performed by comparing a field in one WME against a field in another WME.
/// This enum defines which field to take.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum JoinTestField {
    Field1,
    Field2,
    Field3,
}

/// A test compares a field in the current WME against a field from a WME that is x steps back.
///
/// A Token chain represents a list of matches that belong together. By taking the parent token,
/// one goes back one step into the chain. Each step may contain a WME. The JoinNodeTest defines
/// which WME to compare against the current WME, and defines which fields in the WME's should be
/// compared against eachother.
/// Note that the current WME is a candidate for a token that would be at the top of the chain.
/// This token would be at step 0. The real top token thus becomes step 1, it's parent becomes
/// step 2, etc.
#[derive(Debug, PartialEq, Eq)]
pub struct JoinNodeTest {
    /// The field in the current WME we would like to compare.
    pub f1: JoinTestField,
    /// Number of steps back towards the root-token in order to find the second WME.
    pub condition_number_of_arg2: usize,
    /// The field in the second WME to compare against.
    pub f2: JoinTestField,
}

impl Ord for JoinNodeTest {
    fn cmp(&self, other: &Self) -> Ordering {
        self.condition_number_of_arg2.cmp(&other.condition_number_of_arg2)
    }
}

impl PartialOrd for JoinNodeTest {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.condition_number_of_arg2.partial_cmp(&other.condition_number_of_arg2)
    }
}

/// Match field internally
///
/// check inside condition, to see if we have a test variable('x'), Const('equals'), variable('x')
fn condition_internal_match_field1(condition: &ConditionBody, variable: &String, result: &mut Vec<JoinNodeTest>) {
    // no need to test f1 == f1, only test the other cases
    if let ConditionField::Variable(other_var) = &condition.f2 {
        if *variable == *other_var {
            result.push(JoinNodeTest { f1: JoinTestField::Field1, condition_number_of_arg2: 0, f2: JoinTestField::Field2 });
        }
    }
    if let ConditionField::Variable(other_var) = &condition.f3 {
        if *variable == *other_var {
            result.push(JoinNodeTest { f1: JoinTestField::Field1, condition_number_of_arg2: 0, f2: JoinTestField::Field3 });
        }
    }
}

/// Match field internally
///
/// check inside condition, to see if we have a test variable('x'), Const('equals'), variable('x')
fn condition_internal_match_field2(condition: &ConditionBody, variable: &String, result: &mut Vec<JoinNodeTest>) {
    // we already tested f1 == f2, so why test f2 == f1 ?
    if let ConditionField::Variable(other_var) = &condition.f3 {
        if *variable == *other_var {
            result.push(JoinNodeTest { f1: JoinTestField::Field2, condition_number_of_arg2: 0, f2: JoinTestField::Field3 });
        }
    }
}

/// Compare the current condition against the earlier conditions and find variable fields to match.
///
/// Each condition can contain variables. In order for a chain of conditions to match, each condition must
/// match the same token for the same variable. If condition has a variable field x, each condition in
/// earlier that has a variable x must also bind the same token for that variable x. In the join tests,
/// we store the information which earlier condition uses a variable that is used in condition, by
/// counting the number of times steps between the current condition and the earlier condition.
///
/// Note that we only need to find one earlier match per variable in the current condition, since all conditions
/// in earlier already hold the same constraint to their earlier conditions.
///
/// Tests are returned sorted by distance of the earlier condition to the current condition, closest first,
/// so we can fail-fast and don't need to iterate the token tree further than necessary.
///
/// ```
/// # use phreak_engine::condition::*;
/// use phreak_engine::jointest::*;
///
/// let mut earlier: Vec<&Condition> = Vec::new();
///
/// let condition =
///     ConditionBuilder::new(String::from("car"))
///         .variable_field1(String::from("z")).const_field2(String::from("is")).const_field3(String::from("green"))
///         .build();
/// let tests = get_join_tests_from_condition(&condition, &earlier);
/// earlier.push(&condition);
///
/// assert_eq!(tests, []); // no earlier conditions can match
///
/// let condition =
///     ConditionBuilder::new(String::from("car"))
///         .variable_field1(String::from("z")).const_field2(String::from("is")).const_field3(String::from("big"))
///         .build();
/// let tests = get_join_tests_from_condition(&condition, &earlier);
/// earlier.push(&condition);
///
/// assert_eq!(tests, [JoinNodeTest{ f1: JoinTestField::Field1,
///                                  condition_number_of_arg2 : 1,
///                                  f2: JoinTestField::Field1,
/// }]); // The z in both conditions is the same variable, and must match for both facts to be related
/// ```
pub fn get_join_tests_from_condition(condition: &Condition, earlier: &[&Condition]) -> Vec<JoinNodeTest> {
    let mut result: Vec<JoinNodeTest> = Vec::new();

//    trace!("get_join_tests_from_condition {:?} {:?}", condition, earlier );
    match &condition {
        Condition::Positive(cond) | Condition::Negative(cond) => {
            if let ConditionField::Variable(variable) = &cond.f1 {
                // first check inside self, to see if we have a test variable('x'), Const('equals'), variable('x')
                condition_internal_match_field1(cond, variable, &mut result);
                // now check against earlier conditions
                for (i, other_cond) in earlier.iter().rev().enumerate() {
                    match &other_cond {
                        Condition::Positive(oc) => {
                            if let ConditionField::Variable(other_var) = &oc.f1 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field1, condition_number_of_arg2: i + 1, f2: JoinTestField::Field1 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f2 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field1, condition_number_of_arg2: i + 1, f2: JoinTestField::Field2 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f3 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field1, condition_number_of_arg2: i + 1, f2: JoinTestField::Field3 });
                                    break;
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }

            if let ConditionField::Variable(variable) = &cond.f2 {
                // first check self, but not f2, because f2 == f2 always holds
                condition_internal_match_field2(cond, variable, &mut result);
                // now check against earlier conditions
                for (i, other_cond) in earlier.iter().rev().enumerate() {
                    match &other_cond {
                        Condition::Positive(oc) => {
                            if let ConditionField::Variable(other_var) = &oc.f1 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field2, condition_number_of_arg2: i + 1, f2: JoinTestField::Field1 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f2 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field2, condition_number_of_arg2: i + 1, f2: JoinTestField::Field2 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f3 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field2, condition_number_of_arg2: i + 1, f2: JoinTestField::Field3 });
                                    break;
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }

            if let ConditionField::Variable(variable) = &cond.f3 {
                // now check against earlier conditions
                for (i, other_cond) in earlier.iter().rev().enumerate() {
                    match &other_cond {
                        Condition::Positive(oc) => {
                            if let ConditionField::Variable(other_var) = &oc.f1 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field3, condition_number_of_arg2: i + 1, f2: JoinTestField::Field1 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f2 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field3, condition_number_of_arg2: i + 1, f2: JoinTestField::Field2 });
                                    break;
                                }
                            }
                            if let ConditionField::Variable(other_var) = &oc.f3 {
                                if *variable == *other_var {
                                    result.push(JoinNodeTest { f1: JoinTestField::Field3, condition_number_of_arg2: i + 1, f2: JoinTestField::Field3 });
                                    break;
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
        _ => {}
    }

    result.sort();
//    trace!("get_join_tests_from_condition results {:?}", &result );
    result
}
