#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ActionReference<'a> {
    pub(crate) kind: ActionReferenceKind<'a>,
    pub(crate) expression: &'a str,
    pub(crate) name_offset: usize,
    pub(crate) attribute_offset: Option<usize>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ActionReferenceKind<'a> {
    Attribute { name: &'a str, assignment: bool },
    Qualified { name: &'a str, attribute: &'a str },
    NonLocal { rule: &'a str, attribute: &'a str },
}

pub(crate) type ActionReferenceParser = for<'a> fn(&'a str) -> Vec<ActionReference<'a>>;

pub(crate) fn action_references(body: &str) -> Vec<ActionReference<'_>> {
    let mut references = Vec::new();
    collect_references(body, 0, &mut references);
    references
}

fn collect_references<'a>(
    body: &'a str,
    base_offset: usize,
    references: &mut Vec<ActionReference<'a>>,
) {
    let bytes = body.as_bytes();
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'/' if bytes.get(index + 1) == Some(&b'/') => {
                index = body[index + 2..]
                    .find('\n')
                    .map_or(bytes.len(), |newline| index + 2 + newline + 1);
            }
            b'/' if bytes.get(index + 1) == Some(&b'*') => {
                index = block_comment_end(body, index);
            }
            b'\\' => {
                index += 1;
                if index < bytes.len() {
                    index += next_char_len(body, index);
                }
            }
            b'$' if bytes
                .get(index + 1)
                .is_some_and(|byte| is_identifier_start(*byte)) =>
            {
                index += parse_reference(body, index, base_offset, references);
            }
            _ => index += next_char_len(body, index),
        }
    }
}

fn block_comment_end(body: &str, open: usize) -> usize {
    let bytes = body.as_bytes();
    let mut depth = 1_usize;
    let mut index = open + 2;
    while index + 1 < bytes.len() {
        match &bytes[index..index + 2] {
            b"/*" => {
                depth += 1;
                index += 2;
            }
            b"*/" => {
                depth -= 1;
                index += 2;
                if depth == 0 {
                    return index;
                }
            }
            _ => index += 1,
        }
    }
    bytes.len()
}

fn parse_reference<'a>(
    body: &'a str,
    dollar: usize,
    base_offset: usize,
    references: &mut Vec<ActionReference<'a>>,
) -> usize {
    let name_start = dollar + 1;
    let name_end = identifier_end(body.as_bytes(), name_start);
    let name = &body[name_start..name_end];

    if body[name_end..].starts_with("::") {
        let attribute_start = name_end + 2;
        if body
            .as_bytes()
            .get(attribute_start)
            .is_some_and(|byte| is_identifier_start(*byte))
        {
            let attribute_end = identifier_end(body.as_bytes(), attribute_start);
            let expression_end =
                assignment(body, attribute_end).map_or(attribute_end, |value| value.end);
            references.push(ActionReference {
                kind: ActionReferenceKind::NonLocal {
                    rule: name,
                    attribute: &body[attribute_start..attribute_end],
                },
                expression: &body[dollar..expression_end],
                name_offset: base_offset + name_start,
                attribute_offset: Some(base_offset + attribute_start),
            });
            return expression_end - dollar;
        }
    }

    if body[name_end..].starts_with('.') {
        let attribute_start = name_end + 1;
        if body
            .as_bytes()
            .get(attribute_start)
            .is_some_and(|byte| is_identifier_start(*byte))
        {
            let attribute_end = identifier_end(body.as_bytes(), attribute_start);
            if body.as_bytes().get(attribute_end) != Some(&b'(') {
                references.push(ActionReference {
                    kind: ActionReferenceKind::Qualified {
                        name,
                        attribute: &body[attribute_start..attribute_end],
                    },
                    expression: &body[dollar..attribute_end],
                    name_offset: base_offset + name_start,
                    attribute_offset: Some(base_offset + attribute_start),
                });
                return attribute_end - dollar;
            }
        }
    }

    if let Some(assignment) = assignment(body, name_end) {
        references.push(ActionReference {
            kind: ActionReferenceKind::Attribute {
                name,
                assignment: true,
            },
            expression: &body[dollar..assignment.end],
            name_offset: base_offset + name_start,
            attribute_offset: None,
        });
        collect_references(
            &body[assignment.rhs_start..assignment.rhs_end],
            base_offset + assignment.rhs_start,
            references,
        );
        return assignment.end - dollar;
    }

    references.push(ActionReference {
        kind: ActionReferenceKind::Attribute {
            name,
            assignment: false,
        },
        expression: &body[dollar..name_end],
        name_offset: base_offset + name_start,
        attribute_offset: None,
    });
    name_end - dollar
}

#[derive(Clone, Copy)]
struct Assignment {
    rhs_start: usize,
    rhs_end: usize,
    end: usize,
}

fn assignment(body: &str, operand_end: usize) -> Option<Assignment> {
    let bytes = body.as_bytes();
    let equals = skip_whitespace(bytes, operand_end);
    if bytes.get(equals) != Some(&b'=') || bytes.get(equals + 1) == Some(&b'=') {
        return None;
    }
    let rhs_start = equals + 1;
    let rhs_end = body[rhs_start..]
        .char_indices()
        .skip(1)
        .find_map(|(offset, character)| (character == ';').then_some(rhs_start + offset))?;
    Some(Assignment {
        rhs_start,
        rhs_end,
        end: rhs_end + 1,
    })
}

fn identifier_end(bytes: &[u8], start: usize) -> usize {
    let mut end = start + 1;
    while bytes
        .get(end)
        .is_some_and(|byte| is_identifier_continue(*byte))
    {
        end += 1;
    }
    end
}

const fn is_identifier_start(byte: u8) -> bool {
    byte == b'_' || byte.is_ascii_alphabetic()
}

const fn is_identifier_continue(byte: u8) -> bool {
    byte == b'_' || byte.is_ascii_alphanumeric()
}

fn skip_whitespace(bytes: &[u8], mut index: usize) -> usize {
    while bytes.get(index).is_some_and(u8::is_ascii_whitespace) {
        index += 1;
    }
    index
}

fn next_char_len(text: &str, index: usize) -> usize {
    text[index..].chars().next().map_or(1, char::len_utf8)
}

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

    #[test]
    fn classifies_references_and_tracks_identifier_offsets() {
        let body = "x $value $rule.result $scope::item";
        let references = action_references(body);
        assert_eq!(
            references,
            [
                ActionReference {
                    kind: ActionReferenceKind::Attribute {
                        name: "value",
                        assignment: false,
                    },
                    expression: "$value",
                    name_offset: 3,
                    attribute_offset: None,
                },
                ActionReference {
                    kind: ActionReferenceKind::Qualified {
                        name: "rule",
                        attribute: "result",
                    },
                    expression: "$rule.result",
                    name_offset: 10,
                    attribute_offset: Some(15),
                },
                ActionReference {
                    kind: ActionReferenceKind::NonLocal {
                        rule: "scope",
                        attribute: "item",
                    },
                    expression: "$scope::item",
                    name_offset: 23,
                    attribute_offset: Some(30),
                },
            ],
        );
    }

    #[test]
    fn assignments_follow_action_splitter_rhs_rules() {
        let references = action_references("$q = $blort; $S::j = $S::k; $S::i=$S::i");
        assert_eq!(
            references
                .iter()
                .map(|reference| reference.expression)
                .collect::<Vec<_>>(),
            ["$q = $blort;", "$blort", "$S::j = $S::k;", "$S::i", "$S::i",],
        );
        assert!(matches!(
            references[0].kind,
            ActionReferenceKind::Attribute {
                assignment: true,
                ..
            }
        ));
    }

    #[test]
    fn indexed_nonlocals_and_method_calls_start_as_simple_attributes() {
        let references = action_references("$Q[-1]::y $S[$S::y]::i $ID.getText()");
        assert_eq!(
            references
                .iter()
                .map(|reference| reference.expression)
                .collect::<Vec<_>>(),
            ["$Q", "$S", "$S::y", "$ID"],
        );
    }

    #[test]
    fn escaped_dollars_and_comments_are_text() {
        let references = action_references("\\$x /* $y */ // $z\n$ok");
        assert_eq!(references.len(), 1);
        assert_eq!(references[0].expression, "$ok");
    }

    #[test]
    fn target_syntax_is_not_special_cased() {
        let references = action_references("macro_rules! value { ($i:ident) => { $i } }\n$actual");

        assert_eq!(
            references
                .iter()
                .map(|reference| reference.expression)
                .collect::<Vec<_>>(),
            ["$i", "$i", "$actual"]
        );
    }
}
