use super::*;
use smallvec::SmallVec;

impl<'a> Parser<'a> {
    pub(super) fn fd_var_gap_allows_attachment(gap: &str) -> bool {
        if gap.is_empty() {
            return true;
        }

        let mut remaining = gap;
        while !remaining.is_empty() {
            if let Some(stripped) = remaining.strip_prefix("\\\r\n") {
                remaining = stripped;
                continue;
            }
            if let Some(stripped) = remaining.strip_prefix("\\\n") {
                remaining = stripped;
                continue;
            }
            return false;
        }

        true
    }

    pub(super) fn fd_var_from_text(text: &str, span: Span) -> Option<(Name, Span)> {
        if !text.starts_with('{') || !text.ends_with('}') || text.len() <= 2 {
            return None;
        }

        let inner = &text[1..text.len() - 1];
        if !inner.chars().all(|c| c.is_alphanumeric() || c == '_') {
            return None;
        }

        let start = span.start.advanced_by("{");
        let span = Span::from_positions(start, start.advanced_by(inner));
        Some((Name::from(inner), span))
    }

    pub(super) fn current_fd_var(&mut self) -> Option<(Name, Span)> {
        if let Some(token) = self.current_token.as_ref()
            && token.kind == TokenKind::Word
            && let Some(word) = token.word()
            && let Some(segment) = word.single_segment()
            && segment.kind() == LexedWordSegmentKind::Plain
            && !Self::word_text_needs_parse(segment.as_str())
            && let Some(fd_var) = Self::fd_var_from_text(
                segment.as_str(),
                segment.span().unwrap_or(self.current_span),
            )
        {
            return Some(fd_var);
        }

        let word = self.current_word_ref()?.clone();
        let text = self.literal_word_text(&word)?;
        Self::fd_var_from_text(&text, word.span)
    }

    pub(super) fn is_redirect_kind(kind: TokenKind) -> bool {
        REDIRECT_TOKENS.contains(kind)
    }

    pub(super) fn current_fd_value(&self) -> Option<i32> {
        self.current_token.as_ref().and_then(LexedToken::fd_value)
    }

    pub(super) fn current_fd_pair(&self) -> Option<(i32, i32)> {
        self.current_token
            .as_ref()
            .and_then(LexedToken::fd_pair_value)
    }
    pub(super) fn push_redirect_both_append(
        redirects: &mut SmallVec<[Redirect; 1]>,
        operator_span: Span,
        target: Word,
    ) {
        redirects.push(Redirect {
            fd: None,
            fd_var: None,
            fd_var_span: None,
            kind: RedirectKind::Append,
            span: Self::redirect_span(operator_span, &target),
            target: RedirectTarget::Word(target),
        });
        redirects.push(Redirect {
            fd: Some(2),
            fd_var: None,
            fd_var_span: None,
            kind: RedirectKind::DupOutput,
            span: operator_span,
            target: RedirectTarget::Word(Word::literal("1")),
        });
    }

    pub(super) fn redirect_supports_fd_var(kind: TokenKind) -> bool {
        matches!(
            kind,
            TokenKind::RedirectOut
                | TokenKind::Clobber
                | TokenKind::RedirectAppend
                | TokenKind::RedirectIn
                | TokenKind::RedirectReadWrite
                | TokenKind::HereString
                | TokenKind::RedirectBoth
                | TokenKind::DupOutput
                | TokenKind::DupInput
        )
    }

    pub(super) fn maybe_expect_word(&mut self, strict: bool) -> Result<Option<Word>> {
        if strict {
            self.expect_word().map(Some)
        } else {
            Ok(self.expect_word().ok())
        }
    }

    pub(super) fn consume_non_heredoc_redirect(
        &mut self,
        redirects: &mut SmallVec<[Redirect; 1]>,
        fd_var: Option<Name>,
        fd_var_span: Option<Span>,
        strict: bool,
    ) -> Result<bool> {
        match self.current_token_kind {
            Some(TokenKind::RedirectOut) | Some(TokenKind::Clobber) => {
                let operator_span = self.current_span;
                let kind = if self.at(TokenKind::Clobber) {
                    RedirectKind::Clobber
                } else {
                    RedirectKind::Output
                };
                let fd = if fd_var.is_some() {
                    None
                } else {
                    self.current_fd_value()
                };
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd,
                        fd_var,
                        fd_var_span,
                        kind,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectAppend) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: None,
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::Append,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectIn) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: None,
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::Input,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectReadWrite) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: None,
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::ReadWrite,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::HereString) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: None,
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::HereString,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectBoth) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: None,
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::OutputBoth,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectBothAppend) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    Self::push_redirect_both_append(redirects, operator_span, target);
                }
                Ok(true)
            }
            Some(TokenKind::DupOutput) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: if fd_var.is_some() { None } else { Some(1) },
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::DupOutput,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectFd) => {
                let fd = self.current_fd_value().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: Some(fd),
                        fd_var: None,
                        fd_var_span: None,
                        kind: RedirectKind::Output,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectFdAppend) => {
                let fd = self.current_fd_value().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: Some(fd),
                        fd_var: None,
                        fd_var_span: None,
                        kind: RedirectKind::Append,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::RedirectFdReadWrite) => {
                let fd = self.current_fd_value().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: Some(fd),
                        fd_var: None,
                        fd_var_span: None,
                        kind: RedirectKind::ReadWrite,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::DupFd) => {
                let (src_fd, dst_fd) = self.current_fd_pair().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                redirects.push(Redirect {
                    fd: Some(src_fd),
                    fd_var: None,
                    fd_var_span: None,
                    kind: RedirectKind::DupOutput,
                    span: operator_span,
                    target: RedirectTarget::Word(Word::literal(dst_fd.to_string())),
                });
                Ok(true)
            }
            Some(TokenKind::DupInput) => {
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: if fd_var.is_some() { None } else { Some(0) },
                        fd_var,
                        fd_var_span,
                        kind: RedirectKind::DupInput,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            Some(TokenKind::DupFdIn) => {
                let (src_fd, dst_fd) = self.current_fd_pair().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                redirects.push(Redirect {
                    fd: Some(src_fd),
                    fd_var: None,
                    fd_var_span: None,
                    kind: RedirectKind::DupInput,
                    span: operator_span,
                    target: RedirectTarget::Word(Word::literal(dst_fd.to_string())),
                });
                Ok(true)
            }
            Some(TokenKind::DupFdClose) => {
                let fd = self.current_fd_value().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                redirects.push(Redirect {
                    fd: Some(fd),
                    fd_var: None,
                    fd_var_span: None,
                    kind: RedirectKind::DupInput,
                    span: operator_span,
                    target: RedirectTarget::Word(Word::literal("-")),
                });
                Ok(true)
            }
            Some(TokenKind::RedirectFdIn) => {
                let fd = self.current_fd_value().unwrap_or_default();
                let operator_span = self.current_span;
                self.advance();
                if let Some(target) = self.maybe_expect_word(strict)? {
                    redirects.push(Redirect {
                        fd: Some(fd),
                        fd_var: None,
                        fd_var_span: None,
                        kind: RedirectKind::Input,
                        span: Self::redirect_span(operator_span, &target),
                        target: RedirectTarget::Word(target),
                    });
                }
                Ok(true)
            }
            _ => Ok(false),
        }
    }

    pub(super) fn parse_trailing_redirects(&mut self) -> SmallVec<[Redirect; 1]> {
        let mut redirects = SmallVec::<[Redirect; 1]>::new();
        let mut pending_fd_var = None;
        loop {
            let current_end = self.current_span.end.offset();
            let next_token = self
                .peek_next()
                .map(|token| (token.kind, token.span.start.offset()));
            let input_len = self.input.len();
            if pending_fd_var.is_none()
                && let Some((fd_var, fd_var_span)) = self.current_fd_var()
                && let Some((next_kind, next_start)) = next_token
                && Self::is_redirect_kind(next_kind)
                && current_end <= next_start
                && next_start <= input_len
                && Self::fd_var_gap_allows_attachment(&self.input[current_end..next_start])
            {
                pending_fd_var = Some((fd_var, fd_var_span));
                self.advance();
                continue;
            }

            match self.current_token_kind {
                Some(TokenKind::HereDoc) | Some(TokenKind::HereDocStrip) => {
                    let strip_tabs = self.at(TokenKind::HereDocStrip);
                    let (fd_var, fd_var_span) = pending_fd_var.take().unzip();
                    if !self
                        .consume_heredoc_redirect(
                            strip_tabs,
                            &mut redirects,
                            fd_var,
                            fd_var_span,
                            false,
                            false,
                        )
                        .unwrap_or(false)
                    {
                        break;
                    }
                    continue;
                }
                Some(kind) => {
                    let (fd_var, fd_var_span) = if Self::redirect_supports_fd_var(kind) {
                        pending_fd_var.take().unzip()
                    } else {
                        let _ = pending_fd_var.take();
                        (None, None)
                    };

                    if self
                        .consume_non_heredoc_redirect(&mut redirects, fd_var, fd_var_span, false)
                        .unwrap_or(false)
                    {
                        continue;
                    }
                    break;
                }
                None => break,
            }
        }
        redirects
    }
}
