#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <dirent.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>

enum {
    VERSION = 101
};

const char *wday[] = {
    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

int files;  /* number of files */
char **fn;  /* dynamic 2D array of filenames */
struct stat **at;  /* what stat returns for each file */
int dmode;  /* current display mode */
int onedir, nofile, noexit;

/* function prototypes */

int get_argument(char **done, char *s);

int get_list(void);
void free_list(void);
int find_name(char *s);

int allocate_at(void);

void print_list(char *s, int min, int max, int high);
void print_heading(void);
void print_line(int l);
void display_list(char *done);

void display_dir(void);
void execute_program(char *p, char *a);
void print_help(void);
void print_usage(char *progname);
int is_usage(char *s);

int main(int argc, char *argv[]) {
    char *done = 0;
    int x;

    for(x = 1; x < argc; x ++) {
        if(get_argument(&done, argv[x])) {
            print_usage(argc ? argv[0] : "*unknown*");
            return 1;
        }
    }

    //_djstat_flags = _STAT_EXEC_MAGIC | _STAT_DIRSIZE;

    if(get_list() || allocate_at()) {
        printf("\nMemory allocation error\n");
        free_list();
        return 1;
    }

    display_list(done);
    
    free_list();
    
    return 0;
}

int get_argument(char **done, char *s) {
    if(is_usage(s)) return 1;
    else if(strnicmp(s, "-x", 2) == 0 || strnicmp(s, "/x", 2) == 0
        || strnicmp(s, "x", 1) == 0) {

        if(tolower(s[0]) != 'b') *done = s+2;
        else *done = s+1;
    }
    else if(strnicmp(s, "-dir", 4) == 0 || strnicmp(s, "/dir", 4) == 0
        || strnicmp(s, "dir", 3) == 0) {

        onedir = 1;
    }
    else if(strnicmp(s, "-nodir", 6) == 0 || strnicmp(s, "/nodir", 6) == 0
        || strnicmp(s, "nodir", 5) == 0) {

        onedir = 2;
    }
    else if(strnicmp(s, "-nofile", 7) == 0 || strnicmp(s, "/nofile", 7) == 0
        || strnicmp(s, "nofile", 6) == 0) {

        nofile = 1;
    }
    else if(strnicmp(s, "-noexit", 7) == 0 || strnicmp(s, "/noexit", 7) == 0
        || strnicmp(s, "noexit", 6) == 0) {

        noexit = 1;
    }
    else return 1;

    return 0;
}

int get_list(void) {
    char **tfn;
    int pos, x, fail = 0;
    struct dirent *f;
    DIR *dir;

    dir = opendir(".");
    fn = 0, files = 0, at = 0;

    while((f = readdir(dir))) {
        tfn = realloc(fn, (files+1) * sizeof(char *));
        if(!tfn) {
            fail = 1;
            break;
        }
        fn = tfn;

        pos = find_name(f->d_name);

        for(x = files; x > pos; x --) {
            fn[x] = fn[x-1];
        }

        fn[pos] = malloc(f->d_namlen+1);
        if(!fn[pos]) {
            fail = 1;
            break;
        }
        strcpy(fn[pos], f->d_name);
        
        files ++;
    }

    closedir(dir);

    return fail;
}

void free_list(void) {
    int x;

    for(x = 0; x < files; x ++) {
        /*puts(fn[x]);*/
        free(fn[x]);
        free(at[x]);
    }

    free(fn);
    free(at);
}

int find_name(char *s) {
    int max = files, min = 0, x, a, b;

    if(files) {
        if(stricmp(s, fn[0]) < 0) {
            return 0;
        }
        if(files > 1 && stricmp(s, fn[1]) < 0) {
            return 0;
        }
        
        do {
            a = max, b = min;
            if((x=stricmp(s, fn[(min+max)/2])) < 0) {
                max = (min+max)/2;
            }
            else if(x > 0) {
                min = (min+max)/2;
            }
            else return (min+max)/2;
        } while(a != max || b != min);
    }

    return max;
}

int allocate_at(void) {
    int x;

    at = calloc(sizeof(struct stat *), files);

    return !at;
}

void print_list(char *s, int min, int max, int high) {
    int x = *s?find_name(s):0, y = 0, no = 1;
    struct stat st;
    size_t len = strlen(s);

    x += min;

    print_heading();

    while(x < files && (max < 0 || ++y < max) && (!*s || strnicmp(s, fn[x], len) == 0)) {
        no = 0;
        putchar(high == y-1 ? '>' : ' ');
        putchar(' ');

        if(!at[x]) {
            at[x] = malloc(sizeof(struct stat));
            stat(fn[x], &st);

            if(at[x]) *at[x] = st;
        }

        print_line(x);
        
        if(y < 21) putchar('\n');
        x ++;
    }

    if(x > files || no) printf("    * No matches found *\n");
}

void print_heading(void) {
    int x;

    printf("  ");

    switch(dmode) {
    case 0:
        putchar(218);
        for(x = 0; x < 9; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 9; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277  \332\304\277  "
            "\332\304 Md \304\277 "
            "\332\304 Cr \304\277 "
            "\332\304 Ac \304\277\n");
        break;
    case 1:
        putchar(218);
        for(x = 0; x < 9; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 9; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277  \332\304\277  "
            "\332\304\304\304\304\304 Modified \304\304\304\304\304\277\n");
        break;
    case 2:
        putchar(218);
        for(x = 0; x < 9; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 9; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277  \332\304\277  "
            "\332\304\304\304\304\304 Created \304\304\304\304\304\304\277\n");
        break;
    case 3:
        putchar(218);
        for(x = 0; x < 9; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 9; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277  \332\304\277  "
            "\332\304\304\304\304\304 Accessed \304\304\304\304\304\277\n");
        break;
    case 4:
        putchar(218);
        for(x = 0; x < 26; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 26; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277\n");
        break;
    case 5:
        putchar(218);
        for(x = 0; x < 9; x ++) putchar(196);
        printf(" Filename ");
        for(x = 0; x < 9; x ++) putchar(196);

        printf("\277  \332\304\304 Size \304\277 "
            "\332\304 Size KB \304\277 "
            "\332\304 Size MB \304\277 "
            "\332\304\277\n");
            
        break;
    }
}

void print_line(int l) {
    struct tm *t;

    switch(dmode) {
    case 0:
        printf("%-30.30s%c ", fn[l], strlen(fn[l])>30?'*':' ');
        if(at[l]->st_nlink == 1) printf("%10iB", at[l]->st_size);
        else printf("D%8isd", at[l]->st_nlink-2);

        printf("  ");

        putchar(at[l]->st_mode & S_IRUSR ? 'r' : '-');
        putchar(at[l]->st_mode & S_IWUSR ? 'w' : '-');
        putchar(at[l]->st_mode & S_IXUSR ? 'x' : '-');

        t = localtime(&at[l]->st_mtime);
        printf("  %02i/%02i/%02i", t->tm_mday, t->tm_mon+1, t->tm_year%100);

        t = localtime(&at[l]->st_ctime);
        printf(" %02i/%02i/%02i", t->tm_mday, t->tm_mon+1, t->tm_year%100);

        t = localtime(&at[l]->st_atime);
        printf(" %02i/%02i/%02i", t->tm_mday, t->tm_mon+1, t->tm_year%100);
        
        break;

    case 1:
        printf("%-30.30s%c ", fn[l], strlen(fn[l])>30?'*':' ');
        if(at[l]->st_nlink == 1) printf("%10iB", at[l]->st_size);
        else printf("D%8isd", at[l]->st_nlink-2);

        printf("  ");

        putchar(at[l]->st_mode & S_IRUSR ? 'r' : '-');
        putchar(at[l]->st_mode & S_IWUSR ? 'w' : '-');
        putchar(at[l]->st_mode & S_IXUSR ? 'x' : '-');

        t = localtime(&at[l]->st_mtime);
        printf("  %02i/%02i/%02i %s %2i:%02i:%02i%c", t->tm_mday, t->tm_mon+1,
            t->tm_year%100, wday[t->tm_wday], t->tm_hour%12, t->tm_min, t->tm_sec,
            t->tm_hour > 12 ? 'p' : 'a');

        break;
    case 2:
        printf("%-30.30s%c ", fn[l], strlen(fn[l])>30?'*':' ');
        if(at[l]->st_nlink == 1) printf("%10iB", at[l]->st_size);
        else printf("D%8isd", at[l]->st_nlink-2);

        printf("  ");

        putchar(at[l]->st_mode & S_IRUSR ? 'r' : '-');
        putchar(at[l]->st_mode & S_IWUSR ? 'w' : '-');
        putchar(at[l]->st_mode & S_IXUSR ? 'x' : '-');

        t = localtime(&at[l]->st_ctime);
        printf("  %02i/%02i/%02i %s %2i:%02i:%02i%c", t->tm_mday, t->tm_mon+1,
            t->tm_year%100, wday[t->tm_wday], t->tm_hour%12, t->tm_min, t->tm_sec,
            t->tm_hour > 12 ? 'p' : 'a');

        break;
    case 3:
        printf("%-30.30s%c ", fn[l], strlen(fn[l])>30?'*':' ');
        if(at[l]->st_nlink == 1) printf("%10iB", at[l]->st_size);
        else printf("D%8isd", at[l]->st_nlink-2);

        printf("  ");

        putchar(at[l]->st_mode & S_IRUSR ? 'r' : '-');
        putchar(at[l]->st_mode & S_IWUSR ? 'w' : '-');
        putchar(at[l]->st_mode & S_IXUSR ? 'x' : '-');

        t = localtime(&at[l]->st_atime);
        printf("  %02i/%02i/%02i %s %2i:%02i:%02i%c", t->tm_mday, t->tm_mon+1,
            t->tm_year%100, wday[t->tm_wday], t->tm_hour%12, t->tm_min, t->tm_sec,
            t->tm_hour > 12 ? 'p' : 'a');

        break;
    case 4:
        printf("%-64.64s%c ", fn[l], strlen(fn[l])>64?'*':' ');
        if(at[l]->st_nlink == 1) printf("%10iB", at[l]->st_size);
        else printf("D%8isd", at[l]->st_nlink-2);

        break;
    case 5:
        printf("%-30.30s%c ", fn[l], strlen(fn[l])>30?'*':' ');

        if(at[l]->st_nlink == 1) {
            printf("%10iB", at[l]->st_size);
            printf(" %11.3fKB", at[l]->st_size/1024.0);
            printf(" %10.5fMB", at[l]->st_size/1048576.0);
        }
        else printf("D%8isd                           ", at[l]->st_nlink-2);

        printf("  ");

        putchar(at[l]->st_mode & S_IRUSR ? 'r' : '-');
        putchar(at[l]->st_mode & S_IWUSR ? 'w' : '-');
        putchar(at[l]->st_mode & S_IXUSR ? 'x' : '-');

        break;
    }
}

void display_list(char *done) {
    char pat[70];
    int xp = 0, sxp = 0, last;
    int x, c, mfiles, quit = 0;
    size_t len;

    *pat = 0;

    while(!quit) {
        if(*pat) {
            x = find_name(pat);
            len = strlen(pat);
            for(mfiles = 0; x < files && strnicmp(pat, fn[x++], len) == 0; mfiles ++) ;
        }
        else mfiles = files;
    
        clrscr();
        printf("DF v%i.%02i by DWK    Press F1 for help    "
            "Matches: %i/%i    Selected: %i\n"
            "%s\333\n", VERSION/100, VERSION%100, mfiles, files,
            mfiles ? xp+sxp+1 : 0, pat);
        for(x = 0; x < 80; x ++) putchar(205);
        
        print_list(pat, sxp, 22, xp);
        
        //printf("%i", c);

        /*while(atpos < files && !kbhit()) {
            if(!at[atpos]) {
                at[x] = malloc(sizeof(struct stat));
                stat(fn[x], &st);

                if(at[x]) *at[x] = st;
                else break;
            }

            atpos ++;
        }*/

        c = getch();

        if(!c || c == 224) c = 256 + getch();
        
        switch(c) {
        case 328:  /* up */
            if(xp) xp --;
            else if(sxp) sxp --;
            
            break;
        case 336:  /* down */
            if(xp+sxp < mfiles-1) {
                if(xp < 20) xp ++;
                else sxp ++;
            }
            
            break;
        case 315:  /* F1 */
            print_help();
            break;
        case 327:  /* home */
        case 375:  /* ctrl-home */
            xp = sxp = 0;
            break;
        case 335:  /* end */
        case 373:  /* ctrl-end */
            if(mfiles < 22) xp = mfiles-1, sxp = 0;
            else sxp = mfiles-21, xp = 20;
            break;
        case 388:  /* ctrl-pageup */
            xp = 0;
            break;
        case 374:  /* ctrl-pagedown */
            xp = (mfiles-sxp-1 > 20) ? 20 : (mfiles-sxp-1);
            break;
        case 329:  /* pageup */
            if(xp) xp = 0;
            else if(sxp > 20) sxp -= 20;
            else sxp = 0;
            
            break;
        case 337:  /* pagedown */
            if(xp < 20) {
                xp = (mfiles-sxp-1 > 20) ? 20 : (mfiles-sxp-1);
            }
            else {
                if(mfiles-sxp <= 40) sxp = mfiles-21;
                else sxp += 20;
            }
            
            break;
        case 26:  /* ctrl-z */
            dmode = 0;
            break;
        case 1:  /* ctrl-a */
            dmode = 1;
            break;
        case 24:  /* ctrl-x */
            dmode = 4;
            break;
        case 23:
            display_dir();
            break;
        case 19:  /* ctrl-s */
            dmode = 2;
            break;
        case 4:  /* ctrl-d */
            dmode = 3;
            break;
        case 17:  /* ctrl-q */
            dmode = 5;
            break;
        case 27:
            xp = -1;
            quit = 2;
            break;
        case '\b':
            if(*pat) {
                pat[strlen(pat)-1] = 0;
            }

            break;
        case '\r':
            quit = 1;
            break;
        default:
            if(mfiles && c < 256 && isprint(c)) {
                len = strlen(pat);
                if(len < sizeof(pat)) {
                    pat[len] = c;
                    pat[len+1] = 0;
                    xp = sxp = 0;
                }
            }

            break;
        }

        if(quit == 1) {
            last = find_name(pat);
            if(at[last+sxp+xp]->st_nlink != 1) {
                switch(onedir) {
                case 1:
                    chdir(fn[last+sxp+xp]);
                    
                    free_list();
                    
                    if(get_list() || allocate_at()) {
                        printf("\nMemory allocation error\n");
                        free_list();
                        return;
                    }

                    //display_list(done);
                    quit = 0;
                    xp = sxp = 0;
                    *pat = 0;

                    break;
                case 2:
                    quit = 0;
                    break;
                }
            }
            else if(nofile) quit = 0;
            else if(noexit) {
                execute_program(done, fn[last+sxp+xp]);
                quit = 0;
            }
        }
    }

    clrscr();
    //printf("!");
    if(mfiles && xp >= 0) {
        //x = find_name(pat);
        puts(fn[last+sxp+xp]);
        //printf("[%i]\n", xp);

        if(done) execute_program(done, fn[last+sxp+xp]), puts(fn[last+sxp+xp]);
    }
}

void display_dir(void) {
    char *s = getcwd(0, PATH_MAX);

    clrscr();

    puts("DF:");
    puts(s);

    free(s);

    getch();
}

void execute_program(char *p, char *a) {
    char *s = malloc(strlen(p) + strlen(a) + 2);

    strcpy(s, p);
    strcat(s, " ");
    strcat(s, a);

    system(s);

    free(s);
}

void print_help(void) {
    clrscr();

    printf("DF help\n"
        "\304\304\304\304\304\304\304\n"
        "\nKeys:"
        "\n    [HOME] [CTRL-HOME]  Go to the start of the file list"
        "\n    [END] [CTRL-END]    Go to the end of the file list"
        "\n    [PGDN] [PGUP]       Go down or up a page"
        "\n    [CTRL-PG*]          Go to the top or bottom of the current page"
        "\n\nModes:"
        "\n    [CTRL-Z]  Normal mode   (name, size/subdirs, permissions, md., cr., ac.)"
        "\n    [CTRL-X]  Name mode     (very wide name, size/subdirs)"
        "\n    [CTRL-Q]  Size mode     (name, size/subdirs, size KB, size MB, permissions)"
        "\n    [CTRL-A]  Modified mode (name, size/subdirs, permissions, detailed md.)"
        "\n    [CTRL-S]  Created mode  (name, size/subdirs, permissions, detailed cr.)"
        "\n    [CTRL-D]  Accessed mode (name, size/subdirs, permissions, detailed ac.)"
        "\n\nPress any key to return . . . ");

    getch();
}

void print_usage(char *progname) {
    printf("\nDF v%i.%02i by DWK"
        "\nExecutable path: %s\n"
        "\nusage: df [-xcommand] [-nofile] [-nodir] [-dir]\n"
        "\n-xcommand  Runs command with the selected file"
        "\n-nofile    Makes files not selectable"
        "\n-nodir     Makes diretories not selectable"
        "\n-dir       Makes directories cd-able"
        "\n-noexit    DF doesn't exit after a file is selected\n"
        /*"\n-b  Bare listing (like dir /b)\n"*/,
        VERSION/100, VERSION%100, progname);
}

int is_usage(char *s) {
    return (strchr(s, '?')
        || stricmp(s, "-h") == 0
        || stricmp(s, "-v") == 0
        || stricmp(s, "--help") == 0
        || stricmp(s, "--version") == 0);
}
