#!/usr/bin/env rdmd
//          Copyright Martin Nowak 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)
module dget;

import std.algorithm, std.exception, std.file, std.range, std.net.curl;
static import std.stdio;

void usage()
{
    std.stdio.writeln("usage: dget [--clone|-c] [--help|-h] <repo>...");
}

int main(string[] args)
{
    if (args.length == 1) { usage(); return 1; }

    import std.getopt;
    bool clone, help;
    getopt(args,
           "clone|c", &clone,
           "help|h", &help);

    if (help) { usage(); return 0; }

    import std.meta;
    string user, repo;
    foreach(arg; args[1 .. $])
    {
        AliasSeq!(user, repo) = resolveRepo(arg);
        enforce(!repo.exists, fmt("output folder '%s' already exists", repo));
        if (clone) cloneRepo(user, repo);
        else fetchMaster(user, repo).unzipTo(repo);
    }
    return 0;
}

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

/// default github users for repo lookup
immutable defaultUsers = ["D-Programming-Deimos", "dlang"];

auto resolveRepo(string arg)
{
    import std.regex;

    enum rule = regex(r"^(?:([^/:]*)/)?([^/:]*)$");
    auto m = match(arg, rule);
    enforce(!m.empty, fmt("expected 'user/repo' but found '%s'", arg));

    auto user = m.captures[1];
    auto repo = m.captures[2];

    if (user.empty)
    {
        auto tail = defaultUsers.find!(u => u.hasRepo(repo))();
        enforce(!tail.empty, fmt("repo '%s' was not found among '%(%s, %)'",
                                  repo, defaultUsers));
        user = tail.front;
    }
    import std.typecons;
    return tuple(user, repo);
}

bool hasRepo(string user, string repo)
{
    return fmt("https://api.github.com/users/%s/repos?type=public", user)
        .reqJSON().array.canFind!(a => a.object["name"].str == repo)();
}

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

void cloneRepo(string user, string repo)
{
    import std.process;
    enforce(executeShell(fmt("git clone git://github.com/%s/%s", user, repo)).status == 0);
}

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

ubyte[] fetchMaster(string user, string repo)
{
    auto url = fmt("https://api.github.com/repos/%s/%s/git/refs/heads/master", user, repo);
    auto sha = url.reqJSON().object["object"].object["sha"].str;
    std.stdio.writefln("fetching %s/%s@%s", user, repo, sha);
    return download(fmt("https://github.com/%s/%s/zipball/master", user, repo));
}

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

auto reqJSON(string url)
{
    import std.json;
    return parseJSON(get(url));
}

//..............................................................................

ubyte[] download(string url)
{
    // doesn't work because it already timeouts after 2 minutes
    // return get!(HTTP, ubyte)();

    import core.time, std.array, std.conv;

    auto buf = appender!(ubyte[])();
    size_t contentLength;

    auto http = HTTP(url);
    http.method = HTTP.Method.get;
    http.onReceiveHeader((in k, in v)
    {
        if (k == "content-length")
            contentLength = to!size_t(v);
    });
    http.onReceive((data)
    {
        buf.put(data);
        std.stdio.writef("%sk/%sk\r", buf.data.length/1024,
                         contentLength ? to!string(contentLength/1024) : "?");
        std.stdio.stdout.flush();
        return data.length;
    });
    http.dataTimeout = dur!"msecs"(0);
    http.perform();
    immutable sc = http.statusLine().code;
    enforce(sc / 100 == 2 || sc == 302,
            fmt("HTTP request returned status code %s", sc));
    std.stdio.writeln("done                    ");
    return buf.data;
}

//..............................................................................

void unzipTo(ubyte[] data, string outdir)
{
    import std.path, std.string, std.zip;

    scope archive = new ZipArchive(data);
    std.stdio.writeln("unpacking:");
    string prefix;
    mkdir(outdir);

    foreach(name, _; archive.directory)
    {
        prefix = name[0 .. $ - name.find("/").length + 1];
        break;
    }
    foreach(name, am; archive.directory)
    {
        if (!am.expandedSize) continue;

        string path = buildPath(outdir, chompPrefix(name, prefix));
        std.stdio.writeln(path);
        auto dir = dirName(path);
        if (!dir.empty && !dir.exists)
            mkdirRecurse(dir);
        archive.expand(am);
        std.file.write(path, am.expandedData);
    }
}

//..............................................................................

string fmt(Args...)(string fmt, auto ref Args args)
{
    import std.array, std.format;
    auto app = appender!string();
    formattedWrite(app, fmt, args);
    return app.data;
}
