//! Breaks rustdocs into sections according to their headers

use doc::ItemUtils;

export mk_pass;

fn mk_pass() -> Pass {
    {
        name: ~"sectionalize",
        f: run
    }
}

fn run(_srv: astsrv::Srv, doc: doc::Doc) -> doc::Doc {
    let fold = fold::Fold({
        fold_item: fold_item,
        fold_trait: fold_trait,
        fold_impl: fold_impl,
        .. *fold::default_any_fold(())
    });
    fold.fold_doc(fold, doc)
}

fn fold_item(fold: fold::Fold<()>, doc: doc::ItemDoc) -> doc::ItemDoc {
    let doc = fold::default_seq_fold_item(fold, doc);
    let (desc, sections) = sectionalize(doc.desc);

    {
        desc: desc,
        sections: sections,
        .. doc
    }
}

fn fold_trait(fold: fold::Fold<()>, doc: doc::TraitDoc) -> doc::TraitDoc {
    let doc = fold::default_seq_fold_trait(fold, doc);

    {
        methods: do par::map(doc.methods) |method| {
            let (desc, sections) = sectionalize(method.desc);

            {
                desc: desc,
                sections: sections,
                .. *method
            }
        },
        .. doc
    }
}

fn fold_impl(fold: fold::Fold<()>, doc: doc::ImplDoc) -> doc::ImplDoc {
    let doc = fold::default_seq_fold_impl(fold, doc);

    {
        methods: do par::map(doc.methods) |method| {
            let (desc, sections) = sectionalize(method.desc);

            {
                desc: desc,
                sections: sections,
                .. *method
            }
        },
        .. doc
    }
}

fn sectionalize(desc: Option<~str>) -> (Option<~str>, ~[doc::Section]) {

    /*!
     * Take a description of the form
     *
     *     General text
     *
     *     # Section header
     *
     *     Section text
     *
     *     # Section header
     *
     *     Section text
     *
     * and remove each header and accompanying text into section records.
     */

    if desc.is_none() {
        return (None, ~[]);
    }

    let lines = str::lines(desc.get());

    let mut new_desc = None::<~str>;
    let mut current_section = None;
    let mut sections = ~[];

    for lines.each |line| {
        match parse_header(*line) {
          Some(header) => {
            if current_section.is_some() {
                sections += ~[current_section.get()];
            }
            current_section = Some({
                header: header,
                body: ~""
            });
          }
          None => {
            match copy current_section {
              Some(section) => {
                current_section = Some({
                    body: section.body + ~"\n" + *line,
                    .. section
                });
              }
              None => {
                new_desc = match new_desc {
                  Some(desc) => {
                    Some(desc + ~"\n" + *line)
                  }
                  None => {
                    Some(*line)
                  }
                };
              }
            }
          }
        }
    }

    if current_section.is_some() {
        sections += ~[current_section.get()];
    }

    (new_desc, sections)
}

fn parse_header(line: ~str) -> Option<~str> {
    if str::starts_with(line, ~"# ") {
        Some(str::slice(line, 2u, str::len(line)))
    } else {
        None
    }
}

#[test]
fn should_create_section_headers() {
    let doc = test::mk_doc(
        ~"#[doc = \"\
         # Header\n\
         Body\"]\
         mod a {
             #[legacy_exports]; }");
    assert str::contains(
        doc.cratemod().mods()[0].item.sections[0].header,
        ~"Header");
}

#[test]
fn should_create_section_bodies() {
    let doc = test::mk_doc(
        ~"#[doc = \"\
         # Header\n\
         Body\"]\
         mod a {
             #[legacy_exports]; }");
    assert str::contains(
        doc.cratemod().mods()[0].item.sections[0].body,
        ~"Body");
}

#[test]
fn should_not_create_sections_from_indented_headers() {
    let doc = test::mk_doc(
        ~"#[doc = \"\n\
         Text\n             # Header\n\
         Body\"]\
         mod a {
             #[legacy_exports]; }");
    assert vec::is_empty(doc.cratemod().mods()[0].item.sections);
}

#[test]
fn should_remove_section_text_from_main_desc() {
    let doc = test::mk_doc(
        ~"#[doc = \"\
         Description\n\n\
         # Header\n\
         Body\"]\
         mod a {
             #[legacy_exports]; }");
    assert !str::contains(
        doc.cratemod().mods()[0].desc().get(),
        ~"Header");
    assert !str::contains(
        doc.cratemod().mods()[0].desc().get(),
        ~"Body");
}

#[test]
fn should_eliminate_desc_if_it_is_just_whitespace() {
    let doc = test::mk_doc(
        ~"#[doc = \"\
         # Header\n\
         Body\"]\
         mod a {
             #[legacy_exports]; }");
    assert doc.cratemod().mods()[0].desc() == None;
}

#[test]
fn should_sectionalize_trait_methods() {
    let doc = test::mk_doc(
        ~"trait i {
         #[doc = \"\
         # Header\n\
         Body\"]\
         fn a(); }");
    assert doc.cratemod().traits()[0].methods[0].sections.len() == 1u;
}

#[test]
fn should_sectionalize_impl_methods() {
    let doc = test::mk_doc(
        ~"impl bool {
         #[doc = \"\
         # Header\n\
         Body\"]\
         fn a() { } }");
    assert doc.cratemod().impls()[0].methods[0].sections.len() == 1u;
}

#[cfg(test)]
mod test {
    #[legacy_exports];
    fn mk_doc(source: ~str) -> doc::Doc {
        do astsrv::from_str(source) |srv| {
            let doc = extract::from_srv(srv, ~"");
            let doc = attr_pass::mk_pass().f(srv, doc);
            run(srv, doc)
        }
    }
}
