#include "common.hpp"
#include "dir.hpp"
#include <fcntl.h>
#include <sys/stat.h>


typedef unsigned op_t;
#define OP_NUM_UPDATE_A   1u
#define OP_NUM_UPDATE_B   2u
#define OP_NUM_REPLACE_A  3u
#define OP_NUM_REPLACE_B  4u
#define OP_NUM_CREATE_A   5u
#define OP_NUM_CREATE_B   6u
#define OP_NUM_DELETE_A   7u
#define OP_NUM_DELETE_B   8u
#define OP_NUM_DELETE_ALL 9u
#define OP_UPDATE_A   (1u<<(OP_NUM_UPDATE_A-1))
#define OP_UPDATE_B   (1u<<(OP_NUM_UPDATE_B-1))
#define OP_REPLACE_A  (1u<<(OP_NUM_REPLACE_A-1))
#define OP_REPLACE_B  (1u<<(OP_NUM_REPLACE_B-1))
#define OP_CREATE_A   (1u<<(OP_NUM_CREATE_A-1))
#define OP_CREATE_B   (1u<<(OP_NUM_CREATE_B-1))
#define OP_DELETE_A   (1u<<(OP_NUM_DELETE_A-1))
#define OP_DELETE_B   (1u<<(OP_NUM_DELETE_B-1))
#define OP_DELETE_ALL (1u<<(OP_NUM_DELETE_ALL-1))
#define OP_UNKNOWN    (1u<<OP_NUM_DELETE_ALL)


static struct {
    bool dry_run;
    bool yes_to_all;
    const char* backup_dir[2];
    int backup[2]; // dirfds
    char delete_excluded;
    char keep;
} config = {};


static const char* exclude[] = {
    "*.o",
    ".svn",
    ".bzr",
    ".git",
    "build",
    "Thumbs.db",
    "a.out",
    "tmp",
    "*.tmp",
    "* - tmp",
    "old",
    "bup",
    "backup",
    "*.bup",
    "*.old",
    "*~",
    "~*",
    "*.pyc",
    "__pycache__",
    ".mypy_cache",
    "venv",
    "libraries",
    "Debug",
    "Release",
    "workspace",
    "*.workspace",
    ".project",
    ".idea",
    ".vscode",
    NULL
};


static char get_char() {
    static char rv[16];
    if (!fgets(rv, sizeof(rv), stdin)) *rv = 0;
    return *rv;
}


static unsigned numbits(unsigned n) {
    unsigned rv = 0;
    while (n) {
        rv += n & 1;
        n >>= 1;
    }
    return rv;
}


static op_t confirm(op_t op, const char* desc_a, const char* desc_b, bool choose) {
    assert(op);

    const char* s; // strip common path prefix for legibility
    while ((s = strchr(desc_a, '/')) != NULL) {
        if (strncmp(desc_a, desc_b, s-desc_a+1) != 0) break;
        desc_b += s-desc_a+1;
        desc_a = s+1;
    }

    if (choose && numbits(op) == 1) {
        choose = false;
    }

    if (choose) printf(" 0) skip\n");
    if (op & OP_UPDATE_A)   printf(" %d) " COLOR(YELLOW, "update") " in %s\n", OP_NUM_UPDATE_A, desc_a);
    if (op & OP_UPDATE_B)   printf(" %d) " COLOR(YELLOW, "update") " in %s\n", OP_NUM_UPDATE_B, desc_b);
    if (op & OP_REPLACE_A)  printf(" %d) " COLOR(RED, "replace") " in %s\n", OP_NUM_REPLACE_A, desc_a);
    if (op & OP_REPLACE_B)  printf(" %d) " COLOR(RED, "replace") " in %s\n", OP_NUM_REPLACE_B, desc_b);
    if (op & OP_CREATE_A)   printf(" %d) " COLOR(GREEN, "create") " in %s\n", OP_NUM_CREATE_A, desc_a);
    if (op & OP_CREATE_B)   printf(" %d) " COLOR(GREEN, "create") " in %s\n", OP_NUM_CREATE_B, desc_b);
    if (op & OP_DELETE_A)   printf(" %d) " COLOR(RED, "delete") " in %s\n", OP_NUM_DELETE_A, desc_a);
    if (op & OP_DELETE_B)   printf(" %d) " COLOR(RED, "delete") " in %s\n", OP_NUM_DELETE_B, desc_b);
    if (op & OP_DELETE_ALL) printf(" %d) " COLOR(RED, "delete") " in %s " COLOR(RED, "and") " %s\n", OP_NUM_DELETE_ALL, desc_a, desc_b);

    if (config.dry_run) {
        printf("[skipped %s]\n", choose? "selection": "operation");
        return 0;
    } else if (config.yes_to_all) {
        if (choose) {
            if (!(op & (op - 1))) { // is a power of 2; only one set
                printf(COLOR(YELLOW, "[yes]") "\n");
                return op;
            } else {
                printf(COLOR(RED, "[skipped]") "\n");
                return 0;
            }
        } else {
            printf(COLOR(GREEN, "[yes]") "\n");
            return op;
        }
    }
    while (true) {
        if (choose) {
            printf("[0-9] which one? ");
        } else {
            printf("[y/n/?] all of those? ");
        }
        fflush(stdout);
        char rv = get_char();
        if (choose) {
            if (rv < '0' || rv > '9') continue;
            if (rv == '0') return 0;
            op_t chosen = 1u << ((rv - '0')-1);
            if ((op & chosen) != 0) return chosen;
        } else {
            switch(rv) {
                case 'y':
                    return op;
                case 'n':
                    return 0;
                case '?':
                    return OP_UNKNOWN; // only case for this
            }
            if (rv != 'y' && rv != 'n' && rv != 's') continue;
            return (rv == 'y')? op: 0;
        }
    }
}


static void log_transfer(const Ent* a, const Ent* b, bool success) {
    Ent::print(a, "    ", "\n");
    Ent::print(b, success? COLOR(GREEN, " -> "): COLOR(RED, " X> "), "\n");
}


static void log_info(const Ent* a, const char* desc_a, const Ent* b, const char* desc_b) {
    Ent::print(a, "\n >> ", " @ ");
    puts(desc_a);
    Ent::print(b, " << ", " @ ");
    puts(desc_b);
}


op_t proposal(const Dir* dir_a, const char* desc_a, const Ent* a, const Dir* dir_b, const char* desc_b, const Ent* b) {
    assert(a || b);
    op_t proposal = 0;
    op_t possible = 0;

    bool dir_a_newer = dir_a->attr->mtime() > dir_b->attr->mtime();
    bool dir_b_newer = dir_b->attr->mtime() > dir_a->attr->mtime();

    if ((a && a->type == Ent::TYPE_NONE) || (b && b->type == Ent::TYPE_NONE)) {
        // cannot even delete -> skip
    } else if (matches(exclude, a? a->name: b->name)) { // skip silently or propose a delete if configured
        if (a && (config.delete_excluded == 'a' || config.delete_excluded == '*')) proposal |= OP_DELETE_A;
        if (b && (config.delete_excluded == 'b' || config.delete_excluded == '*')) proposal |= OP_DELETE_B;
        if (proposal) { // chose to possibly delete instead of silently skip, so propose other side's deletion, too
            if (a) possible |= OP_DELETE_A;
            if (b) possible |= OP_DELETE_B;
        }
    } else if (!a || !b) { // create/delete
        if (dir_a_newer) {
            proposal |= a? OP_CREATE_B: OP_DELETE_B;
        } else if (dir_b_newer) {
            proposal |= b? OP_CREATE_A: OP_DELETE_A;
        }
        possible |= a? OP_DELETE_A|OP_CREATE_B: OP_CREATE_A|OP_DELETE_B;
    } else { // both exist: update?
        switch (a->cmp(b)) {
            case Attr::FAIL:
                break;
            case Attr::SAME:
                break;
            case Attr::ATTR_THIS:
                proposal |= OP_UPDATE_B;
                possible |= OP_UPDATE_A;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::ATTR_THAT:
                proposal |= OP_UPDATE_A;
                possible |= OP_UPDATE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::ATTR_CONFLICT:
                possible |= OP_UPDATE_A;
                possible |= OP_UPDATE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::FULL_THIS:
                proposal |= OP_REPLACE_B;
                possible |= OP_REPLACE_A;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::FULL_THAT:
                proposal |= OP_REPLACE_A;
                possible |= OP_REPLACE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::FULL_OR_ATTR_THIS:
                proposal |= OP_REPLACE_B;
                possible |= OP_REPLACE_A;
                possible |= OP_UPDATE_A|OP_UPDATE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::FULL_OR_ATTR_THAT:
                proposal |= OP_REPLACE_A;
                possible |= OP_REPLACE_B;
                possible |= OP_UPDATE_A|OP_UPDATE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
            case Attr::FULL_CONFLICT:
                possible |= OP_REPLACE_A;
                possible |= OP_REPLACE_B;
                possible |= OP_DELETE_A|OP_DELETE_B;
                break;
        }
    }

    if (config.keep == 'a') {
        proposal &= ~(OP_UPDATE_A|OP_REPLACE_A|OP_CREATE_A|OP_DELETE_A);
        possible &= ~(OP_UPDATE_A|OP_REPLACE_A|OP_CREATE_A|OP_DELETE_A);
    } else if (config.keep == 'b') {
        proposal &= ~(OP_UPDATE_B|OP_REPLACE_B|OP_CREATE_B|OP_DELETE_B);
        possible &= ~(OP_UPDATE_B|OP_REPLACE_B|OP_CREATE_B|OP_DELETE_B);
    }

    possible |= proposal;
    if ((possible & OP_DELETE_A) && (possible & OP_DELETE_B)) {
        possible |= OP_DELETE_ALL;
    }
    if (!possible) {
        return 0; // silently
    }

    log_info(a, desc_a, b, desc_b);
    if (proposal) {
        op_t chosen = confirm(proposal, desc_a, desc_b, false);
        if (chosen != OP_UNKNOWN) {
            return chosen;
        }
    }
    return confirm(possible, desc_a, desc_b, true);
}


static bool run(DirStack* a, const char* desc_a, DirStack* b, const char* desc_b) {
    Dir* dir_a = a->get();
    Dir* dir_b = b->get();
    assert(dir_a && dir_b);

    DirEntries* ls_a = dir_a->entries;
    DirEntries* ls_b = dir_b->entries;

    while (true) {
        Ent* ent_a = ls_a->pop();
        Ent* ent_b = ls_b->pop();
        if (!ent_a && !ent_b) {
            break;
        }

        if (ent_a && ent_b && strcmp(ent_a->name, ent_b->name) != 0) {
            if (DirEntries::before(ent_a, ent_b)) { // b does not contain a's name
                ls_b->unpop(ent_b);
                ent_b = NULL;
            } else { // a does not contain b's name
                ls_a->unpop(ent_a);
                ent_a = NULL;
            }
        }

        op_t op = proposal(dir_a, desc_a, ent_a, dir_b, desc_b, ent_b);
        if (config.dry_run) {
            assert(!op);
        }
        if (op & OP_DELETE_ALL) {
            op &= ~OP_DELETE_ALL;
            op |= OP_DELETE_A|OP_DELETE_B;
        }
        if (op & OP_UPDATE_A) {
            if (!ent_a->apply(ent_b)) {
                log_transfer(ent_b, ent_a, false);
                goto err;
            } else {
                log_transfer(ent_b, ent_a, true);
            }
        }
        if (op & OP_UPDATE_B) {
            if (!ent_b->apply(ent_a)) {
                log_transfer(ent_a, ent_b, false);
                goto err;
            } else {
                log_transfer(ent_a, ent_b, true);
            }
        }
        if (op & OP_DELETE_A) {
            if (!ent_a->delInst(config.backup[0])) {
                log_transfer(ent_a, NULL, false);
                goto err;
            } else {
                log_transfer(ent_a, NULL, true);
            }
            delete ent_a;
            ent_a = NULL;
        }
        if (op & OP_DELETE_B) {
            if (!ent_b->delInst(config.backup[1])) {
                log_transfer(ent_b, NULL, false);
                goto err;
            } else {
                log_transfer(ent_b, NULL, true);
            }
            delete ent_b;
            ent_b = NULL;
        }
        if (op & OP_CREATE_A) {
            ent_a = Ent::createInst(dir_a->fd, ent_b->name, ent_b->type == Ent::TYPE_DIR);
            if (!ent_a || (ent_b->type == Ent::TYPE_REG && !ent_a->read_from(ent_b)) || !ent_a->apply(ent_b)) {
                log_transfer(ent_b, ent_a, false);
                goto err;
            }
            log_transfer(ent_b, ent_a, true);
        }
        if (op & OP_CREATE_B) {
            ent_b = Ent::createInst(dir_b->fd, ent_a->name, ent_a->type == Ent::TYPE_DIR);
            if (!ent_b || (ent_b->type == Ent::TYPE_REG && !ent_b->read_from(ent_a)) || !ent_b->apply(ent_a)) {
                log_transfer(ent_a, ent_b, false);
                goto err;
            }
            log_transfer(ent_a, ent_b, true);
        }
        if (op & OP_REPLACE_A) {
            if (!ent_a->read_from(ent_b, config.backup[0]) || !ent_a->apply(ent_b)) {
                log_transfer(ent_b, ent_a, false);
                goto err;
            }
            log_transfer(ent_b, ent_a, true);
        }
        if (op & OP_REPLACE_B) {
            if (!ent_b->read_from(ent_a, config.backup[1]) || !ent_b->apply(ent_a)) {
                log_transfer(ent_a, ent_b, false);
                goto err;
            }
            log_transfer(ent_a, ent_b, true);
        }

        if (ent_a && ent_b && ent_a->type == Ent::TYPE_DIR && !matches(exclude, ent_a->name)) {
            assert(ent_b->type == Ent::TYPE_DIR);
            if (!a->push(ent_a->name)) {
                goto err;
            }
            if (!b->push(ent_b->name)) {
                a->pop();
                goto err;
            }
            char* next_desc_a = NULL;
            UNUSED(asprintf(&next_desc_a, "%s/%s", desc_a, ent_a->name));
            char* next_desc_b = NULL;
            UNUSED(asprintf(&next_desc_b, "%s/%s", desc_b, ent_b->name));
            if (!run(a, next_desc_a, b, next_desc_b)) {
                free(next_desc_a);
                free(next_desc_b);
                goto err;
            }
            free(next_desc_a);
            free(next_desc_b);
        }

        if (ent_a) delete ent_a;
        if (ent_b) delete ent_b;
        continue;
        err: // TODO: ask for retry or skip
        if (ent_a) delete ent_a;
        if (ent_b) delete ent_b;
        a->pop();
        b->pop();
        return false;
    }

    a->pop();
    b->pop();
    return true;
}


int main(int argc, char **argv) {
    if (argc < 3) {
        LOG("usage: %s [--delete-excluded[-a|-b]] [--keep-(a|b)] [--dry-run] [--backup[-a|-b]=dirname] a/ b/", argv[0]);
        return 1;
    }
    const char* const arga = argv[argc-2];
    const char* const argb = argv[argc-1];
    for (int i=1; i<argc-2; ++i) {
        bool override = false;
        if (!strcmp(argv[i], "--delete-excluded-a")) {
            if (config.delete_excluded != 0) override = true;
            config.delete_excluded = 'a';
        } else if (!strcmp(argv[i], "--delete-excluded-b")) {
            if (config.delete_excluded != 0) override = true;
            config.delete_excluded = 'b';
        } else if (!strcmp(argv[i], "--delete-excluded")) {
            if (config.delete_excluded != 0) override = true;
            config.delete_excluded = '*';
        } else if (!strcmp(argv[i], "--dry-run")) {
            if (config.dry_run) override = true;
            config.dry_run = true;
        } else if (!strcmp(argv[i], "--yes-to-all")) {
            if (config.yes_to_all) override = true;
            config.yes_to_all = true;
        } else if (!strcmp(argv[i], "--keep-a")) {
            if (config.keep != 0) override = true;
            config.keep = 'a';
        } else if (!strcmp(argv[i], "--keep-b")) {
            if (config.keep != 0) override = true;
            config.keep = 'b';
        } else if (!strncmp(argv[i], "--backup=", 9)) {
            if (config.backup_dir[0] || config.backup_dir[1]) override = true;
            config.backup_dir[0] = config.backup_dir[1] = argv[i] + 9;
        } else if (!strncmp(argv[i], "--backup-a=", 11)) {
            if (config.backup_dir[0]) override = true;
            config.backup_dir[0] = argv[i] + 11;
        } else if (!strncmp(argv[i], "--backup-b=", 11)) {
            if (config.backup_dir[1]) override = true;
            config.backup_dir[1] = argv[i] + 11;
        } else {
            LOG("unknown arg '%s'", argv[i]);
            return 1;
        }
        if (override) {
            LOG("value for arg '%s' has already been set", argv[i]);
            return 1;
        }
    }

    LOG("starting up%s...", config.dry_run? " (DRY-RUN)": "");
    DirStack a(arga);
    if (!a.get()) return 1;
    DirStack b(argb);
    if (!b.get()) return 1;

    if (config.backup_dir[0]) {
        if (mkdirat(a.get()->fd, config.backup_dir[0], S_IRUSR|S_IWUSR|S_IXUSR) == -1) { // XXX: race
            LOG_ERRNO("mkdirat(%s, %s)", arga, config.backup_dir[0]);
            return 1;
        }
        config.backup[0] = openat(a.get()->fd, config.backup_dir[0], O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
        if (config.backup[0] == -1) {
            LOG_ERRNO("openat(%s, %s)", arga, config.backup_dir[0]);
            return 1;
        }
    } else {
        config.backup[0] = -1;
    }
    if (config.backup_dir[1]) {
        if (mkdirat(b.get()->fd, config.backup_dir[1], S_IRUSR|S_IWUSR|S_IXUSR) == -1) {
            LOG_ERRNO("mkdirat(%s, %s)", argb, config.backup_dir[1]);
            return 1;
        }
        config.backup[1] = openat(b.get()->fd, config.backup_dir[1], O_RDONLY|O_DIRECTORY|O_NOFOLLOW);
        if (config.backup[1] == -1) {
            LOG_ERRNO("openat(%s, %s)", argb, config.backup_dir[1]);
            return 1;
        }
    } else {
        config.backup[1] = -1;
    }

    bool rv = run(&a, arga, &b, argb);
    LOG("Done: %s", rv? "success": "fail");

#if 0
    if (config.dry_run && rv) {
        printf("re-run with '--yes-to-all'? [y/n] ");
        fflush(stdout);
        char c = get_char();
        if (c == 'y') {
            config.dry_run = false;
            config.yes_to_all = true;
            assert(!a.get() && !b.get());
            a.push(arga);
            a.push(argb);
            if (!a.get() || !b.get()) return 1;
            rv = run(&a, arga, &b, argb);
            LOG("Done: %s", rv? "success": "fail");
        }
    }
#endif

    return rv? 0: 1;
}