use result::Result;
use std::getopts;

export OutputFormat;
export OutputStyle;
export Config;
export default_config;
export parse_config;
export usage;
export Markdown, PandocHtml;
export DocPerCrate, DocPerMod;

/// The type of document to output
enum OutputFormat {
    /// Markdown
    Markdown,
    /// HTML, via markdown and pandoc
    PandocHtml
}

impl OutputFormat : cmp::Eq {
    pure fn eq(other: &OutputFormat) -> bool {
        (self as uint) == ((*other) as uint)
    }
    pure fn ne(other: &OutputFormat) -> bool { !self.eq(other) }
}

/// How to organize the output
enum OutputStyle {
    /// All in a single document
    DocPerCrate,
    /// Each module in its own document
    DocPerMod
}

impl OutputStyle : cmp::Eq {
    pure fn eq(other: &OutputStyle) -> bool {
        (self as uint) == ((*other) as uint)
    }
    pure fn ne(other: &OutputStyle) -> bool { !self.eq(other) }
}

/// The configuration for a rustdoc session
type Config = {
    input_crate: Path,
    output_dir: Path,
    output_format: OutputFormat,
    output_style: OutputStyle,
    pandoc_cmd: Option<~str>
};

fn opt_output_dir() -> ~str { ~"output-dir" }
fn opt_output_format() -> ~str { ~"output-format" }
fn opt_output_style() -> ~str { ~"output-style" }
fn opt_pandoc_cmd() -> ~str { ~"pandoc-cmd" }
fn opt_help() -> ~str { ~"h" }

fn opts() -> ~[(getopts::Opt, ~str)] {
    ~[
        (getopts::optopt(opt_output_dir()),
         ~"--output-dir <val>     put documents here"),
        (getopts::optopt(opt_output_format()),
         ~"--output-format <val>  either 'markdown' or 'html'"),
        (getopts::optopt(opt_output_style()),
         ~"--output-style <val>   either 'doc-per-crate' or 'doc-per-mod'"),
        (getopts::optopt(opt_pandoc_cmd()),
         ~"--pandoc-cmd <val>     the command for running pandoc"),
        (getopts::optflag(opt_help()),
         ~"-h                     print help")
    ]
}

fn usage() {
    use io::println;

    println(~"Usage: rustdoc [options] <cratefile>\n");
    println(~"Options:\n");
    for opts().each |opt| {
        println(fmt!("    %s", opt.second()));
    }
    println(~"");
}

fn default_config(input_crate: &Path) -> Config {
    {
        input_crate: *input_crate,
        output_dir: Path("."),
        output_format: PandocHtml,
        output_style: DocPerMod,
        pandoc_cmd: None
    }
}

type ProgramOutput = fn~((&str), (&[~str])) ->
    {status: int, out: ~str, err: ~str};

fn mock_program_output(_prog: &str, _args: &[~str]) -> {
    status: int, out: ~str, err: ~str
} {
    {
        status: 0,
        out: ~"",
        err: ~""
    }
}

fn parse_config(args: ~[~str]) -> Result<Config, ~str> {
    parse_config_(args, run::program_output)
}

fn parse_config_(
    args: ~[~str],
    program_output: ProgramOutput
) -> Result<Config, ~str> {
    let args = vec::tail(args);
    let opts = vec::unzip(opts()).first();
    match getopts::getopts(args, opts) {
        result::Ok(matches) => {
            if vec::len(matches.free) == 1u {
                let input_crate = Path(vec::head(matches.free));
                config_from_opts(&input_crate, matches, program_output)
            } else if vec::is_empty(matches.free) {
                result::Err(~"no crates specified")
            } else {
                result::Err(~"multiple crates specified")
            }
        }
        result::Err(f) => {
            result::Err(getopts::fail_str(f))
        }
    }
}

fn config_from_opts(
    input_crate: &Path,
    matches: getopts::Matches,
    program_output: ProgramOutput
) -> Result<Config, ~str> {

    let config = default_config(input_crate);
    let result = result::Ok(config);
    let result = do result::chain(result) |config| {
        let output_dir = getopts::opt_maybe_str(matches, opt_output_dir());
        let output_dir = output_dir.map(|s| Path(*s));
        result::Ok({
            output_dir: output_dir.get_default(config.output_dir),
            .. config
        })
    };
    let result = do result::chain(result) |config| {
        let output_format = getopts::opt_maybe_str(
            matches, opt_output_format());
        do output_format.map_default(result::Ok(config))
            |output_format| {
            do result::chain(parse_output_format(*output_format))
                |output_format| {

                result::Ok({
                    output_format: output_format,
                    .. config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let output_style =
            getopts::opt_maybe_str(matches, opt_output_style());
        do output_style.map_default(result::Ok(config))
            |output_style| {
            do result::chain(parse_output_style(*output_style))
                |output_style| {
                result::Ok({
                    output_style: output_style,
                    .. config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let pandoc_cmd = getopts::opt_maybe_str(matches, opt_pandoc_cmd());
        let pandoc_cmd = maybe_find_pandoc(
            config, pandoc_cmd, program_output);
        do result::chain(pandoc_cmd) |pandoc_cmd| {
            result::Ok({
                pandoc_cmd: pandoc_cmd,
                .. config
            })
        }
    };
    return result;
}

fn parse_output_format(output_format: ~str) -> Result<OutputFormat, ~str> {
    match output_format {
      ~"markdown" => result::Ok(Markdown),
      ~"html" => result::Ok(PandocHtml),
      _ => result::Err(fmt!("unknown output format '%s'", output_format))
    }
}

fn parse_output_style(output_style: ~str) -> Result<OutputStyle, ~str> {
    match output_style {
      ~"doc-per-crate" => result::Ok(DocPerCrate),
      ~"doc-per-mod" => result::Ok(DocPerMod),
      _ => result::Err(fmt!("unknown output style '%s'", output_style))
    }
}

fn maybe_find_pandoc(
    config: Config,
    maybe_pandoc_cmd: Option<~str>,
    program_output: ProgramOutput
) -> Result<Option<~str>, ~str> {
    if config.output_format != PandocHtml {
        return result::Ok(maybe_pandoc_cmd);
    }

    let possible_pandocs = match maybe_pandoc_cmd {
      Some(pandoc_cmd) => ~[pandoc_cmd],
      None => {
        ~[~"pandoc"] + match os::homedir() {
          Some(dir) => {
            ~[dir.push_rel(&Path(".cabal/bin/pandoc")).to_str()]
          }
          None => ~[]
        }
      }
    };

    let pandoc = do vec::find(possible_pandocs) |pandoc| {
        let output = program_output(*pandoc, ~[~"--version"]);
        debug!("testing pandoc cmd %s: %?", *pandoc, output);
        output.status == 0
    };

    if pandoc.is_some() {
        result::Ok(pandoc)
    } else {
        result::Err(~"couldn't find pandoc")
    }
}

#[test]
fn should_find_pandoc() {
    let config = {
        output_format: PandocHtml,
        .. default_config(&Path("test"))
    };
    let mock_program_output = fn~(_prog: &str, _args: &[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 0, out: ~"pandoc 1.8.2.1", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, None, mock_program_output);
    assert result == result::Ok(Some(~"pandoc"));
}

#[test]
fn should_error_with_no_pandoc() {
    let config = {
        output_format: PandocHtml,
        .. default_config(&Path("test"))
    };
    let mock_program_output = fn~(_prog: &str, _args: &[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 1, out: ~"", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, None, mock_program_output);
    assert result == result::Err(~"couldn't find pandoc");
}

#[cfg(test)]
mod test {
    #[legacy_exports];
    fn parse_config(args: ~[~str]) -> Result<Config, ~str> {
        parse_config_(args, mock_program_output)
    }
}

#[test]
fn should_error_with_no_crates() {
    let config = test::parse_config(~[~"rustdoc"]);
    assert config.get_err() == ~"no crates specified";
}

#[test]
fn should_error_with_multiple_crates() {
    let config =
        test::parse_config(~[~"rustdoc", ~"crate1.rc", ~"crate2.rc"]);
    assert config.get_err() == ~"multiple crates specified";
}

#[test]
fn should_set_output_dir_to_cwd_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert config.get().output_dir == Path(".");
}

#[test]
fn should_set_output_dir_if_provided() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-dir", ~"snuggles"
    ]);
    assert config.get().output_dir == Path("snuggles");
}

#[test]
fn should_set_output_format_to_pandoc_html_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert config.get().output_format == PandocHtml;
}

#[test]
fn should_set_output_format_to_markdown_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"markdown"
    ]);
    assert config.get().output_format == Markdown;
}

#[test]
fn should_set_output_format_to_pandoc_html_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"html"
    ]);
    assert config.get().output_format == PandocHtml;
}

#[test]
fn should_error_on_bogus_format() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"bogus"
    ]);
    assert config.get_err() == ~"unknown output format 'bogus'";
}

#[test]
fn should_set_output_style_to_doc_per_mod_by_default() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert config.get().output_style == DocPerMod;
}

#[test]
fn should_set_output_style_to_one_doc_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-crate"
    ]);
    assert config.get().output_style == DocPerCrate;
}

#[test]
fn should_set_output_style_to_doc_per_mod_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-mod"
    ]);
    assert config.get().output_style == DocPerMod;
}

#[test]
fn should_error_on_bogus_output_style() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"bogus"
    ]);
    assert config.get_err() == ~"unknown output style 'bogus'";
}

#[test]
fn should_set_pandoc_command_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--pandoc-cmd", ~"panda-bear-doc"
    ]);
    assert config.get().pandoc_cmd == Some(~"panda-bear-doc");
}

#[test]
fn should_set_pandoc_command_when_using_pandoc() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert config.get().pandoc_cmd == Some(~"pandoc");
}
