//! Finds crate binaries and loads their metadata

import syntax::diagnostic::span_handler;
import syntax::{ast, attr};
import syntax::print::pprust;
import syntax::codemap::span;
import lib::llvm::{False, llvm, mk_object_file, mk_section_iter};
import filesearch::filesearch;
import io::writer_util;

export os;
export os_macos, os_win32, os_linux, os_freebsd;
export ctxt;
export load_library_crate;
export list_file_metadata;
export note_linkage_attrs;
export crate_name_from_metas;
export metadata_matches;
export meta_section_name;

enum os {
    os_macos,
    os_win32,
    os_linux,
    os_freebsd
}

type ctxt = {
    diag: span_handler,
    filesearch: filesearch,
    span: span,
    ident: ast::ident,
    metas: ~[@ast::meta_item],
    hash: str,
    os: os,
    static: bool
};

fn load_library_crate(cx: ctxt) -> {ident: str, data: @~[u8]} {
    alt find_library_crate(cx) {
      some(t) { ret t; }
      none {
        cx.diag.span_fatal(
            cx.span, #fmt["can't find crate for '%s'", *cx.ident]);
      }
    }
}

fn find_library_crate(cx: ctxt) -> option<{ident: str, data: @~[u8]}> {
    attr::require_unique_names(cx.diag, cx.metas);
    find_library_crate_aux(cx, libname(cx), cx.filesearch)
}

fn libname(cx: ctxt) -> {prefix: str, suffix: str} {
    if cx.static { ret {prefix: "lib", suffix: ".rlib"}; }
    alt cx.os {
      os_win32 { ret {prefix: "", suffix: ".dll"}; }
      os_macos { ret {prefix: "lib", suffix: ".dylib"}; }
      os_linux { ret {prefix: "lib", suffix: ".so"}; }
      os_freebsd { ret {prefix: "lib", suffix: ".so"}; }
    }
}

fn find_library_crate_aux(cx: ctxt,
                          nn: {prefix: str, suffix: str},
                          filesearch: filesearch::filesearch) ->
   option<{ident: str, data: @~[u8]}> {
    let crate_name = crate_name_from_metas(cx.metas);
    let prefix: str = nn.prefix + *crate_name + "-";
    let suffix: str = nn.suffix;

    let mut matches = ~[];
    filesearch::search(filesearch, |path| {
        #debug("inspecting file %s", path);
        let f: str = path::basename(path);
        if !(str::starts_with(f, prefix) && str::ends_with(f, suffix)) {
            #debug("skipping %s, doesn't look like %s*%s", path, prefix,
                   suffix);
            option::none::<()>
        } else {
            #debug("%s is a candidate", path);
            alt get_metadata_section(cx.os, path) {
              option::some(cvec) {
                if !crate_matches(cvec, cx.metas, cx.hash) {
                    #debug("skipping %s, metadata doesn't match", path);
                    option::none::<()>
                } else {
                    #debug("found %s with matching metadata", path);
                    vec::push(matches, {ident: path, data: cvec});
                    option::none::<()>
                }
              }
              _ {
                #debug("could not load metadata for %s", path);
                option::none::<()>
              }
            }
        }
    });

    if matches.is_empty() {
        none
    } else if matches.len() == 1u {
        some(matches[0])
    } else {
        cx.diag.span_err(
            cx.span, #fmt("multiple matching crates for `%s`", *crate_name));
        cx.diag.handler().note("candidates:");
        for matches.each |match| {
            cx.diag.handler().note(#fmt("path: %s", match.ident));
            let attrs = decoder::get_crate_attributes(match.data);
            note_linkage_attrs(cx.diag, attrs);
        }
        cx.diag.handler().abort_if_errors();
        none
    }
}

fn crate_name_from_metas(metas: ~[@ast::meta_item]) -> @str {
    let name_items = attr::find_meta_items_by_name(metas, "name");
    alt vec::last_opt(name_items) {
      some(i) {
        alt attr::get_meta_item_value_str(i) {
          some(n) { n }
          // FIXME (#2406): Probably want a warning here since the user
          // is using the wrong type of meta item.
          _ { fail }
        }
      }
      none { fail "expected to find the crate name" }
    }
}

fn note_linkage_attrs(diag: span_handler, attrs: ~[ast::attribute]) {
    for attr::find_linkage_attrs(attrs).each |attr| {
        diag.handler().note(#fmt("meta: %s", pprust::attr_to_str(attr)));
    }
}

fn crate_matches(crate_data: @~[u8], metas: ~[@ast::meta_item],
                 hash: str) -> bool {
    let attrs = decoder::get_crate_attributes(crate_data);
    let linkage_metas = attr::find_linkage_metas(attrs);
    if hash.is_not_empty() {
        let chash = decoder::get_crate_hash(crate_data);
        if *chash != hash { ret false; }
    }
    metadata_matches(linkage_metas, metas)
}

fn metadata_matches(extern_metas: ~[@ast::meta_item],
                    local_metas: ~[@ast::meta_item]) -> bool {

    #debug("matching %u metadata requirements against %u items",
           vec::len(local_metas), vec::len(extern_metas));

    #debug("crate metadata:");
    for extern_metas.each |have| {
        #debug("  %s", pprust::meta_item_to_str(*have));
    }

    for local_metas.each |needed| {
        #debug("looking for %s", pprust::meta_item_to_str(*needed));
        if !attr::contains(extern_metas, needed) {
            #debug("missing %s", pprust::meta_item_to_str(*needed));
            ret false;
        }
    }
    ret true;
}

fn get_metadata_section(os: os,
                        filename: str) -> option<@~[u8]> unsafe {
    let mb = str::as_c_str(filename, |buf| {
        llvm::LLVMRustCreateMemoryBufferWithContentsOfFile(buf)
                                   });
    if mb as int == 0 { ret option::none::<@~[u8]>; }
    let of = alt mk_object_file(mb) {
        option::some(of) { of }
        _ { ret option::none::<@~[u8]>; }
    };
    let si = mk_section_iter(of.llof);
    while llvm::LLVMIsSectionIteratorAtEnd(of.llof, si.llsi) == False {
        let name_buf = llvm::LLVMGetSectionName(si.llsi);
        let name = unsafe { str::unsafe::from_c_str(name_buf) };
        if str::eq(name, meta_section_name(os)) {
            let cbuf = llvm::LLVMGetSectionContents(si.llsi);
            let csz = llvm::LLVMGetSectionSize(si.llsi) as uint;
            unsafe {
                let cvbuf: *u8 = unsafe::reinterpret_cast(cbuf);
                ret some(@vec::unsafe::from_buf(cvbuf, csz));
            }
        }
        llvm::LLVMMoveToNextSection(si.llsi);
    }
    ret option::none::<@~[u8]>;
}

fn meta_section_name(os: os) -> str {
    alt os {
      os_macos { "__DATA,__note.rustc" }
      os_win32 { ".note.rustc" }
      os_linux { ".note.rustc" }
      os_freebsd { ".note.rustc" }
    }
}

// A diagnostic function for dumping crate metadata to an output stream
fn list_file_metadata(os: os, path: str, out: io::writer) {
    alt get_metadata_section(os, path) {
      option::some(bytes) { decoder::list_crate_metadata(bytes, out); }
      option::none {
        out.write_str("could not find metadata in " + path + ".\n");
      }
    }
}
