/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include "hal/hal_flash.h"
#include "testutil/testutil.h"
#include "fs/fs.h"
#include "nffs/nffs.h"
#include "nffs_test.h"
#include "nffs_test_priv.h"
#include "nffs_priv.h"

int print_verbose;

void
print_inode_entry(struct nffs_inode_entry *inode_entry, int indent)
{
    struct nffs_inode inode;
    char name[NFFS_FILENAME_MAX_LEN + 1];
    uint32_t area_offset;
    uint8_t area_idx;
    int rc;

    if (inode_entry == nffs_root_dir) {
        printf("%*s/\n", indent, "");
        return;
    }

    rc = nffs_inode_from_entry(&inode, inode_entry);
    /*
     * Dummy inode
     */
    if (rc == FS_ENOENT) {
        printf("    DUMMY %d\n", rc);
        return;
    }

    nffs_flash_loc_expand(inode_entry->nie_hash_entry.nhe_flash_loc,
                         &area_idx, &area_offset);


    rc = nffs_flash_read(area_idx,
                         area_offset + sizeof (struct nffs_disk_inode),
                         name, inode.ni_filename_len);

    name[inode.ni_filename_len] = '\0';

    printf("%*s%s %ju %jx\n", indent, "", name[0] == '\0' ? "/" : name,
           (uintmax_t) inode.ni_seq,
           (uintmax_t) inode.ni_inode_entry->nie_flags);
}

void
process_inode_entry(struct nffs_inode_entry *inode_entry, int indent)
{
    struct nffs_inode_entry *child;

    print_inode_entry(inode_entry, indent);

    if (nffs_hash_id_is_dir(inode_entry->nie_hash_entry.nhe_id)) {
        SLIST_FOREACH(child, &inode_entry->nie_child_list, nie_sibling_next) {
            process_inode_entry(child, indent + 2);
        }
    }
}

int
print_nffs_flash_inode(struct nffs_area *area, uint32_t off)
{
    struct nffs_disk_inode ndi;
    char filename[128];
    int len;
    int rc;

    rc = hal_flash_read(area->na_flash_id, area->na_offset + off,
                         &ndi, sizeof(ndi));
    assert(rc == 0);

    memset(filename, 0, sizeof(filename));
    len = min(sizeof(filename) - 1, ndi.ndi_filename_len);
    rc = hal_flash_read(area->na_flash_id, area->na_offset + off + sizeof(ndi),
                         filename, len);

    printf("  off %jx %s id %jx flen %ju seq %ju last %jx prnt %jx "
           "flgs %jx %s\n",
           (uintmax_t) off,
           (nffs_hash_id_is_file(ndi.ndi_id) ? "File" :
            (nffs_hash_id_is_dir(ndi.ndi_id) ? "Dir" : "???")),
           (uintmax_t) ndi.ndi_id,
           (uintmax_t) ndi.ndi_filename_len,
           (uintmax_t) ndi.ndi_seq,
           (uintmax_t) ndi.ndi_lastblock_id,
           (uintmax_t) ndi.ndi_parent_id,
           (uintmax_t) ndi.ndi_flags,
           filename);
    return sizeof(ndi) + ndi.ndi_filename_len;
}

int
print_nffs_flash_block(struct nffs_area *area, uint32_t off)
{
    struct nffs_disk_block ndb;
    int rc;

    rc = hal_flash_read(area->na_flash_id, area->na_offset + off,
                        &ndb, sizeof(ndb));
    assert(rc == 0);

    printf("  off %jx Block id %jx len %ju seq %ju prev %jx own ino %jx\n",
           (uintmax_t) off,
           (uintmax_t) ndb.ndb_id,
           (uintmax_t) ndb.ndb_data_len,
           (uintmax_t) ndb.ndb_seq,
           (uintmax_t) ndb.ndb_prev_id,
           (uintmax_t) ndb.ndb_inode_id);
    return sizeof(ndb) + ndb.ndb_data_len;
}

int
print_nffs_flash_object(struct nffs_area *area, uint32_t off)
{
    struct nffs_disk_object ndo;

    hal_flash_read(area->na_flash_id, area->na_offset + off,
                        &ndo.ndo_un_obj, sizeof(ndo.ndo_un_obj));

    if (nffs_hash_id_is_inode(ndo.ndo_disk_inode.ndi_id)) {
        return print_nffs_flash_inode(area, off);

    } else if (nffs_hash_id_is_block(ndo.ndo_disk_block.ndb_id)) {
        return print_nffs_flash_block(area, off);

    } else if (ndo.ndo_disk_block.ndb_id == 0xffffffff) {
        return area->na_length;

    } else {
        return 1;
    }
}

void
print_nffs_flash_areas(int verbose)
{
    struct nffs_area area;
    struct nffs_disk_area darea;
    int off;
    int i;

    for (i = 0; nffs_current_area_descs[i].nad_length != 0; i++) {
        if (i > NFFS_MAX_AREAS) {
            return;
        }
        area.na_offset = nffs_current_area_descs[i].nad_offset;
        area.na_length = nffs_current_area_descs[i].nad_length;
        area.na_flash_id = nffs_current_area_descs[i].nad_flash_id;
        hal_flash_read(area.na_flash_id, area.na_offset, &darea, sizeof(darea));
        area.na_id = darea.nda_id;
        area.na_cur = nffs_areas[i].na_cur;
        if (!nffs_area_magic_is_set(&darea)) {
            printf("Area header corrupt!\n");
        }
        printf("area %d: id %ju %jx-%jx cur %jx len %ju flashid %jx "
               "gc-seq %jd %s%s\n",
               i,
               (uintmax_t)area.na_id,
               (uintmax_t)area.na_offset,
               (uintmax_t)area.na_offset + area.na_length,
               (uintmax_t)area.na_cur,
               (uintmax_t)area.na_length,
               (uintmax_t)area.na_flash_id,
               (uintmax_t)darea.nda_gc_seq,
               nffs_scratch_area_idx == i ? "(scratch)" : "",
               !nffs_area_magic_is_set(&darea) ? "corrupt" : "");
        if (verbose < 2) {
            off = sizeof (struct nffs_disk_area);
            while (off < area.na_length) {
                off += print_nffs_flash_object(&area, off);
            }
        }
    }
}

static int
nffs_hash_fn(uint32_t id)
{
    return id % NFFS_HASH_SIZE;
}

void
print_hashlist(struct nffs_hash_entry *he)
{
    struct nffs_hash_list *list;
    int idx = nffs_hash_fn(he->nhe_id);
    list = nffs_hash + idx;

    SLIST_FOREACH(he, list, nhe_next) {
        printf("hash_entry %s %p: id 0x%jx flash_loc 0x%jx next %p\n",
                   nffs_hash_id_is_inode(he->nhe_id) ? "inode" : "block",
                   he,
                   (uintmax_t)he->nhe_id,
                   (uintmax_t)he->nhe_flash_loc,
                   he->nhe_next.sle_next);
   }
}

void
print_hash(void)
{
    int i;
    struct nffs_hash_entry *he;
    struct nffs_hash_entry *next;
    struct nffs_inode ni;
    struct nffs_disk_inode di;
    struct nffs_block nb;
    struct nffs_disk_block db;
    uint32_t area_offset;
    uint8_t area_idx;
    int rc;

    NFFS_HASH_FOREACH(he, i, next) {
        if (nffs_hash_id_is_inode(he->nhe_id)) {
            printf("hash_entry inode %d %p: id 0x%jx flash_loc 0x%jx "
                   "next %p\n",
                   i,
                   he,
                   (uintmax_t)he->nhe_id,
                   (uintmax_t)he->nhe_flash_loc,
                   he->nhe_next.sle_next);
            if (he->nhe_id == NFFS_ID_ROOT_DIR) {
                continue;
            }
            nffs_flash_loc_expand(he->nhe_flash_loc,
                                  &area_idx, &area_offset);
            rc = nffs_inode_read_disk(area_idx, area_offset, &di);
            if (rc) {
                printf("%d: fail inode read id 0x%jx rc %d\n",
                       i, (uintmax_t)he->nhe_id, rc);
            }
            printf("    Disk inode: id %jx seq %ju parent %jx last %jx "
                   "flgs %jx\n",
                   (uintmax_t)di.ndi_id,
                   (uintmax_t)di.ndi_seq,
                   (uintmax_t)di.ndi_parent_id,
                   (uintmax_t)di.ndi_lastblock_id,
                   (uintmax_t)di.ndi_flags);
            ni.ni_inode_entry = (struct nffs_inode_entry *)he;
            ni.ni_seq = di.ndi_seq; 
            ni.ni_parent = nffs_hash_find_inode(di.ndi_parent_id);
            printf("    RAM inode: entry %p seq %ju parent %p "
                   "filename %s\n",
                   ni.ni_inode_entry,
                   (uintmax_t)ni.ni_seq,
                   ni.ni_parent,
                   ni.ni_filename);

        } else if (nffs_hash_id_is_block(he->nhe_id)) {
            printf("hash_entry block %d %p: id 0x%jx flash_loc 0x%jx "
                   "next %p\n",
                   i,
                   he,
                   (uintmax_t)he->nhe_id,
                   (uintmax_t)he->nhe_flash_loc,
                   he->nhe_next.sle_next);
            rc = nffs_block_from_hash_entry(&nb, he);
            if (rc) {
                printf("%d: fail block read id 0x%jx rc %d\n",
                       i, (uintmax_t)he->nhe_id, rc);
            }
            printf("    block: id %jx seq %ju inode %jx prev %jx\n",
                   (uintmax_t)nb.nb_hash_entry->nhe_id,
                   (uintmax_t)nb.nb_seq, 
                   (uintmax_t)nb.nb_inode_entry->nie_hash_entry.nhe_id, 
                   (uintmax_t)nb.nb_prev->nhe_id);
            nffs_flash_loc_expand(nb.nb_hash_entry->nhe_flash_loc,
                                  &area_idx, &area_offset);
            rc = nffs_block_read_disk(area_idx, area_offset, &db);
            if (rc) {
                printf("%d: fail disk block read id 0x%jx rc %d\n",
                       i, (uintmax_t)nb.nb_hash_entry->nhe_id, rc);
            }
            printf("    disk block: id %jx seq %ju inode %jx prev %jx "
                   "len %ju\n",
                   (uintmax_t)db.ndb_id,
                   (uintmax_t)db.ndb_seq,
                   (uintmax_t)db.ndb_inode_id,
                   (uintmax_t)db.ndb_prev_id,
                   (uintmax_t)db.ndb_data_len);
        } else {
            printf("hash_entry UNKNONN %d %p: id 0x%jx flash_loc 0x%jx "
                   "next %p\n",
                   i,
                   he,
                   (uintmax_t)he->nhe_id,
                   (uintmax_t)he->nhe_flash_loc,
                   he->nhe_next.sle_next);
        }
    }

}

void
nffs_print_object(struct nffs_disk_object *dobj)
{
    struct nffs_disk_inode *di = &dobj->ndo_disk_inode;
    struct nffs_disk_block *db = &dobj->ndo_disk_block;

    if (dobj->ndo_type == NFFS_OBJECT_TYPE_INODE) {
        printf("    %s id %jx seq %ju prnt %jx last %jx\n",
               nffs_hash_id_is_file(di->ndi_id) ? "File" :
                nffs_hash_id_is_dir(di->ndi_id) ? "Dir" : "???",
               (uintmax_t)di->ndi_id,
               (uintmax_t)di->ndi_seq,
               (uintmax_t)di->ndi_parent_id,
               (uintmax_t)di->ndi_lastblock_id);
    } else if (dobj->ndo_type != NFFS_OBJECT_TYPE_BLOCK) {
        printf("    %s: id %jx seq %ju ino %jx prev %jx len %ju\n",
               nffs_hash_id_is_block(db->ndb_id) ? "Block" : "Block?",
               (uintmax_t)db->ndb_id,
               (uintmax_t)db->ndb_seq,
               (uintmax_t)db->ndb_inode_id,
               (uintmax_t)db->ndb_prev_id,
               (uintmax_t)db->ndb_data_len);
    }
}

void
print_nffs_hash_block(struct nffs_hash_entry *he, int verbose)
{
    struct nffs_block nb = { 0 };
    struct nffs_disk_block db = { 0 };
    uint32_t area_offset = 0;
    uint8_t area_idx = 0;
    int rc;

    if (he == NULL) {
        return;
    }
    if (!nffs_hash_entry_is_dummy(he)) {
        nffs_flash_loc_expand(he->nhe_flash_loc,
                              &area_idx, &area_offset);
        rc = nffs_block_read_disk(area_idx, area_offset, &db);
        if (rc) {
            printf("%p: fail block read id 0x%jx rc %d\n",
                   he, (uintmax_t)he->nhe_id, rc);

        }
        nb.nb_hash_entry = he;
        nb.nb_seq = db.ndb_seq;
        if (db.ndb_inode_id != NFFS_ID_NONE) {
            nb.nb_inode_entry = nffs_hash_find_inode(db.ndb_inode_id);
        } else {
            nb.nb_inode_entry = (void *)(uintptr_t)db.ndb_inode_id;
        }
        if (db.ndb_prev_id != NFFS_ID_NONE) {
            nb.nb_prev = nffs_hash_find_block(db.ndb_prev_id);
        } else {
            nb.nb_prev = (void *)(uintptr_t)db.ndb_prev_id;
        }
        nb.nb_data_len = db.ndb_data_len;
    }
    if (!verbose) {
        printf("%s%s id %jx idx/off %ju/%jx seq %ju ino %jx prev %jx "
               "len %ju\n",
               nffs_hash_entry_is_dummy(he) ? "Dummy " : "",
               nffs_hash_id_is_block(he->nhe_id) ? "Block" : "Unknown",
               (uintmax_t)he->nhe_id,
               (uintmax_t)area_idx,
               (uintmax_t)area_offset,
               (uintmax_t)nb.nb_seq,
               (uintmax_t)(nb.nb_inode_entry ? nb.nb_inode_entry->nie_hash_entry.nhe_id : 0),
               (uintmax_t)db.ndb_prev_id,
               (uintmax_t)db.ndb_data_len);
        return;
    }
    printf("%s%s id %jx loc %jx/%jx %jx ent %p\n",
           nffs_hash_entry_is_dummy(he) ? "Dummy " : "",
           nffs_hash_id_is_block(he->nhe_id) ? "Block:" : "Unknown:",
           (uintmax_t)he->nhe_id,
           (uintmax_t)area_idx,
           (uintmax_t)area_offset,
           (uintmax_t)he->nhe_flash_loc,
           he);
    if (nb.nb_inode_entry) {
        printf("  Ram: ent %p seq %ju ino %p prev %p len %ju\n",
               nb.nb_hash_entry,
               (uintmax_t)nb.nb_seq,
               nb.nb_inode_entry,
               nb.nb_prev,
               (uintmax_t)nb.nb_data_len);
    }
    if (db.ndb_id) {
        printf("  Disk %s id %jx seq %ju ino %jx prev %jx len %ju\n",
               nffs_hash_id_is_block(db.ndb_id) ? "Block:" : "???:",
               (uintmax_t)db.ndb_id,
               (uintmax_t)db.ndb_seq,
               (uintmax_t)db.ndb_inode_id,
               (uintmax_t)db.ndb_prev_id,
               (uintmax_t)db.ndb_data_len);
    }
}

void
print_nffs_hash_inode(struct nffs_hash_entry *he, int verbose)
{
    struct nffs_inode ni = { 0 };
    struct nffs_disk_inode di = { 0 };
    struct nffs_inode_entry *nie = (struct nffs_inode_entry*)he;
    int cached_name_len;
    uint32_t area_offset = 0;
    uint8_t area_idx = 0;
    int rc = 0;

    if (he == NULL) {
        return;
    }
    if (!nffs_hash_entry_is_dummy(he)) {
        nffs_flash_loc_expand(he->nhe_flash_loc,
                              &area_idx, &area_offset);
        rc = nffs_inode_read_disk(area_idx, area_offset, &di);
        if (rc) {
            printf("Entry %p: fail inode read id 0x%jx rc %d\n",
                   he, (uintmax_t)he->nhe_id, rc);
        }
        ni.ni_inode_entry = (struct nffs_inode_entry *)he;
        ni.ni_seq = di.ndi_seq; 
        if (di.ndi_parent_id != NFFS_ID_NONE) {
            ni.ni_parent = nffs_hash_find_inode(di.ndi_parent_id);
        } else {
            ni.ni_parent = NULL;
        }
        if (ni.ni_filename_len > NFFS_SHORT_FILENAME_LEN) {
            cached_name_len = NFFS_SHORT_FILENAME_LEN;
        } else {
            cached_name_len = ni.ni_filename_len;
        }
        if (cached_name_len != 0) {
            rc = nffs_flash_read(area_idx, area_offset + sizeof di,
                         ni.ni_filename, cached_name_len);
            if (rc != 0) {
                printf("entry %p: fail filename read id 0x%jx rc %d\n",
                       he, (uintmax_t)he->nhe_id, rc);
                return;
            }
        }
    }
    if (!verbose) {
        printf("%s%s id %jx idx/off %jx/%jx seq %ju prnt %jx last %jx "
               "flags %jx",
               nffs_hash_entry_is_dummy(he) ? "Dummy " : "",
               nffs_hash_id_is_file(he->nhe_id) ? "File" :
                he->nhe_id == NFFS_ID_ROOT_DIR ? "**ROOT Dir" : 
                nffs_hash_id_is_dir(he->nhe_id) ? "Dir" : "Inode",

               (uintmax_t)he->nhe_id,
               (uintmax_t)area_idx,
               (uintmax_t)area_offset,
               (uintmax_t)ni.ni_seq,
               (uintmax_t)di.ndi_parent_id,
               (uintmax_t)di.ndi_lastblock_id,
               (uintmax_t)nie->nie_flags);
        if (ni.ni_inode_entry) {
            printf(" ref %ju\n", (uintmax_t)ni.ni_inode_entry->nie_refcnt);
        } else {
            printf("\n");
        }
        return;
    }
    printf("%s%s id %jx loc %jx/%jx %jx entry %p\n",
           nffs_hash_entry_is_dummy(he) ? "Dummy " : "",
           nffs_hash_id_is_file(he->nhe_id) ? "File:" :
            he->nhe_id == NFFS_ID_ROOT_DIR ? "**ROOT Dir:" : 
            nffs_hash_id_is_dir(he->nhe_id) ? "Dir:" : "Inode:",
           (uintmax_t)he->nhe_id,
           (uintmax_t)area_idx,
           (uintmax_t)area_offset,
           (uintmax_t)he->nhe_flash_loc,
           he);
    if (ni.ni_inode_entry) {
        printf("  ram: ent %p seq %ju prnt %p lst %p ref %ju flgs %jx nm %s\n",
               ni.ni_inode_entry,
               (uintmax_t)ni.ni_seq,
               ni.ni_parent,
               ni.ni_inode_entry->nie_last_block_entry,
               (uintmax_t)ni.ni_inode_entry->nie_refcnt,
               (uintmax_t)ni.ni_inode_entry->nie_flags,
               ni.ni_filename);
    }
    if (rc == 0) {
        printf("  Disk %s: id %jx seq %ju prnt %jx lst %jx flgs %jx\n",
               nffs_hash_id_is_file(di.ndi_id) ? "File" :
                nffs_hash_id_is_dir(di.ndi_id) ? "Dir" : "???",
               (uintmax_t)di.ndi_id,
               (uintmax_t)di.ndi_seq,
               (uintmax_t)di.ndi_parent_id,
               (uintmax_t)di.ndi_lastblock_id,
               (uintmax_t)di.ndi_flags);
    }
}

void
print_hash_entries(int verbose)
{
    int i;
    struct nffs_hash_entry *he;
    struct nffs_hash_entry *next;

    printf("\nnffs_hash_entries:\n");
    for (i = 0; i < NFFS_HASH_SIZE; i++) {
        he = SLIST_FIRST(nffs_hash + i);
        while (he != NULL) {
            next = SLIST_NEXT(he, nhe_next);
            if (nffs_hash_id_is_inode(he->nhe_id)) {
                print_nffs_hash_inode(he, verbose);
            } else if (nffs_hash_id_is_block(he->nhe_id)) {
                print_nffs_hash_block(he, verbose);
            } else {
                printf("UNKNOWN type hash entry %d: id 0x%jx loc 0x%jx\n",
                       i,
                       (uintmax_t)he->nhe_id,
                       (uintmax_t)he->nhe_flash_loc);
            }
            he = next;
        }
    }
}

void
print_nffs_hashlist(int verbose)
{
    struct nffs_hash_entry *he;
    struct nffs_hash_entry *next;
    int i;

    NFFS_HASH_FOREACH(he, i, next) {
        if (nffs_hash_id_is_inode(he->nhe_id)) {
            print_nffs_hash_inode(he, verbose);
        } else if (nffs_hash_id_is_block(he->nhe_id)) {
            print_nffs_hash_block(he, verbose);
        } else {
            printf("UNKNOWN type hash entry %d: id 0x%jx loc 0x%jx\n",
                   i,
                   (uintmax_t)he->nhe_id,
                   (uintmax_t)he->nhe_flash_loc);
        }
    }
}

void
printfs()
{
    if (nffs_misc_ready()) {
        printf("NFFS directory:\n");
        process_inode_entry(nffs_root_dir, print_verbose);

        printf("\nNFFS hash list:\n");
        print_nffs_hashlist(print_verbose);
    }
    printf("\nNFFS flash areas:\n");
    print_nffs_flash_areas(print_verbose);
}
