use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::space0;
use nom::combinator::not;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::preceded;
use nom::sequence::tuple;

use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::org_line_ending;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line;
use crate::types::FixedWidthArea;
use crate::types::Keyword;

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn fixed_width_area<'b, 'g, 'r, 's, AK>(
    affiliated_keywords: AK,
    remaining: OrgSource<'s>,
    context: RefContext<'b, 'g, 'r, 's>,
    input: OrgSource<'s>,
) -> Res<OrgSource<'s>, FixedWidthArea<'s>>
where
    AK: IntoIterator<Item = Keyword<'s>>,
{
    let fixed_width_area_line_matcher = parser_with_context!(fixed_width_area_line)(context);
    let exit_matcher = parser_with_context!(exit_matcher_parser)(context);
    let (remaining, first_line) = fixed_width_area_line_matcher(remaining)?;
    let (remaining, mut remaining_lines) =
        many0(preceded(not(exit_matcher), fixed_width_area_line_matcher))(remaining)?;

    let (remaining, _trailing_ws) =
        maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
    let source = get_consumed(input, remaining);
    let mut value = Vec::with_capacity(remaining_lines.len() + 1);
    let last_line = remaining_lines.pop();
    if let Some(last_line) = last_line {
        value.push(Into::<&str>::into(first_line));
        value.extend(remaining_lines.into_iter().map(Into::<&str>::into));
        let last_line = Into::<&str>::into(last_line);
        // Trim the line ending from the final line.
        value.push(&last_line[..(last_line.len() - 1)])
    } else {
        // Trim the line ending from the only line.
        let only_line = Into::<&str>::into(first_line);
        value.push(&only_line[..(only_line.len() - 1)])
    }
    Ok((
        remaining,
        FixedWidthArea {
            source: source.into(),
            affiliated_keywords: parse_affiliated_keywords(
                context.get_global_settings(),
                affiliated_keywords,
            ),
            value,
        },
    ))
}

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(ret, level = "debug", skip(_context))
)]
fn fixed_width_area_line<'b, 'g, 'r, 's>(
    _context: RefContext<'b, 'g, 'r, 's>,
    input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
    start_of_line(input)?;
    let (remaining, _) = tuple((space0, tag(":")))(input)?;
    if let Ok((remaining, line_break)) = org_line_ending(remaining) {
        return Ok((remaining, line_break));
    }
    let (remaining, _) = tag(" ")(remaining)?;
    let (remaining, value) = recognize(many_till(anychar, org_line_ending))(remaining)?;
    Ok((remaining, value))
}

#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_fixed_width_area<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
    let (input, _) = many0(affiliated_keyword)(input)?;
    tuple((
        start_of_line,
        space0,
        tag(":"),
        alt((tag(" "), org_line_ending)),
    ))(input)?;
    Ok((input, ()))
}