#include "pkgman.hh"
#include "misc.hh"
#include "progress.hh"
#include <dirent.h>
#include <sys/stat.h>
#include <time.h>
#include <zlib.h>

// for lvledit command line tool
#ifdef _NO_TMS
    #include <stdlib.h>
    #include <stdio.h>
    #include <cinttypes>

    #define tms_infof(...)
    #define tms_warnf(...)
    #define tms_fatalf(...)
    #define tms_errorf(...)

    static const char *tms_storage_path() { return "/tmp/"; }
    static const char *tms_storage_cache_path() { return "/tmp/"; }
#else
    #include <tms/cpp.hh>
#endif

#include <cinttypes>

static char _level_path[4][1024];
static char _state_path[1024];
static char _cache_path[4][1024];
static char _cache_state_path[1024];
static char _pkg_path[4][1024];
static const char *_dir_names[] = {
    "local", "db", "main", "sys"
};

lvlbuf tmpbuf;
lvlinfo tmplvl;

void lvlinfo::create(int type, uint64_t seed, uint32_t version) {
    this->local_id = 0;
    this->save_id = 0;
    this->version = (version ? version : LEVEL_VERSION);
    this->type = type;
    this->name[0] = '\0';
    this->name_len = 0;

    if (this->type == LCAT_PARTIAL) {
        return;
    }

    this->community_id = 0;
    this->autosave_id = 0;
    this->revision = 0;
    this->parent_id = 0;
    this->parent_revision = 0;
    this->pause_on_finish = true;
    this->show_score = (type == LCAT_ADVENTURE);
    this->descr_len = 0;
    this->visibility = LEVEL_VISIBLE;

    static const int random_bgs[] = {
        0,1,2,4,8,9,10,11,12,13,14
    };
    static const int random_bgs_adventure[] = {
        0,1,2,4,5,8,9,10,11,12,13,14
    };

    if (type == LCAT_ADVENTURE) {
        this->bg = random_bgs_adventure[rand()%(sizeof(random_bgs_adventure)/sizeof(int))];
    } else {
        this->bg = random_bgs[rand()%(sizeof(random_bgs)/sizeof(int))];
    }

    this->bg_color = 0;

    this->descr = 0;
    this->size_x[0] = 40;
    this->size_x[1] = 40;
    this->size_y[0] = 4;
    this->size_y[1] = 10;
    this->gravity_x = 0;
    this->gravity_y = -20.f;
    this->flags = 0;
    this->min_x = 0;
    this->max_x = 0;
    this->min_y = 0;
    this->max_y = 0;
    this->final_score = 0;
    memset(this->icon, 0, 128*128);

    this->sandbox_cam_x = 0.f;
    this->sandbox_cam_y = 0.f;
    this->sandbox_cam_zoom = 12.f;

    this->velocity_iterations = 10;
    this->position_iterations = 10;

    this->seed = 0;
    this->adventure_id = 0xffffffff;

    this->prismatic_tolerance = 0.0125f;
    this->pivot_tolerance = 0.0125f;

    this->linear_damping = 0.1f;
    this->angular_damping = 0.2f;
    this->joint_friction = 0.3f;
    if (type == LCAT_ADVENTURE) {
        this->dead_enemy_absorb_time = 60.0f;
    } else {
        this->dead_enemy_absorb_time = 10.0f;
    }
    this->time_before_player_can_respawn = 1.5f;
    this->compression_length = 0;

    this->flags |= LVL_ALLOW_RESPAWN_WITHOUT_CHECKPOINT;

    if (type == LCAT_ADVENTURE) {
        this->flags |= LVL_ABSORB_DEAD_ENEMIES;
        this->flags |= LVL_ENABLE_INTERACTIVE_DESTRUCTION;
        this->flags |= LVL_DEAD_CREATURE_DESTRUCTION;

        if (seed) {
            this->flags |= LVL_CHUNKED_LEVEL_LOADING;
            this->flags |= LVL_ALLOW_QUICKSAVING;
            this->show_score = false;
            this->seed = seed;
            this->size_y[0] = 500;
            this->bg = 5;
        }
    }

    this->num_groups = 0;
    this->num_entities = 0;
    this->num_connections = 0;
    this->num_cables = 0;
    this->num_chunks = 0;
    this->state_size = 0;
    this->num_gentypes = 0;
}

void lvlinfo::sanity_check() {
    if (this->version < LEVEL_VERSION_1_5) {
        // remove flags added in 1.5
        this->flags &= ~(
                LVL_CHUNKED_LEVEL_LOADING |
                LVL_DISABLE_CAVEVIEW |
                LVL_DISABLE_ROCKET_TRIGGER_EXPLOSIVES |
                LVL_STORE_SCORE_ON_GAME_OVER |
                LVL_ALLOW_HIGH_SCORE_SUBMISSIONS |
                LVL_LOWER_SCORE_IS_BETTER |
                LVL_DISABLE_ENDSCREENS |
                LVL_ALLOW_QUICKSAVING |
                LVL_DEAD_CREATURE_DESTRUCTION
            );

        // add flags added in 1.5 that must be enabled by default
        this->flags |= (
                LVL_ALLOW_RESPAWN_WITHOUT_CHECKPOINT
            );

        this->dead_enemy_absorb_time = 10.0f;
        this->time_before_player_can_respawn = 1.5f;
        this->linear_damping = 0.0f;
        this->angular_damping = 0.0f;
        this->joint_friction = 0.0f;
        this->seed = 0;
        this->adventure_id = 0xffffffff;
        this->compression_length = 0;
    }
}

int lvlinfo::get_size() const {
    return sizeof(uint8_t)  // version
         + sizeof(uint8_t)  // type
         + sizeof(uint32_t) // community_id
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(uint32_t) : 0) // autosave_id
         + sizeof(uint32_t) // revision
         + sizeof(uint32_t) // parent_id
         + sizeof(uint8_t)  // name_len
         + sizeof(uint16_t) // descr_len
         + sizeof(uint8_t)  // allow_derivatives
         + (this->version >= 3 ? sizeof(uint8_t) : 0) // visibility
         + (this->version >= 7 ? sizeof(uint32_t) : 0) // parent_revision
         + (this->version >= 7 ? sizeof(uint8_t) : 0) // pause_on_finish
         + (this->version >= 7 ? sizeof(uint8_t) : 0) // show_score

         + sizeof(uint8_t)  // bg
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(uint32_t) : 0) // bg color, stored as uint32_t

         + sizeof(uint16_t)  // size_x
         + sizeof(uint16_t)  // size_y
         + (this->version>=12 ? sizeof(uint16_t) : 0)  // size_x 2
         + (this->version>=12 ? sizeof(uint16_t) : 0)  // size_y 2

         + sizeof(uint8_t)  // velocity_iterations
         + sizeof(uint8_t)  // position_iterations
         + sizeof(uint32_t)  // final_score
         + sizeof(float)  // sandbox_cam_x
         + sizeof(float)  // sandbox_cam_y
         + sizeof(float)  // sandbox_cam_zoom
         + (this->version >= 3 ? sizeof(float) : 0)  // gravity_x
         + (this->version >= 3 ? sizeof(float) : 0)  // gravity_y
         + (this->version >= 13 ? sizeof(float)*4 : 0) // bounds
         + (this->version >= 9 ? sizeof(uint64_t) : 0)  // flags

         + (this->version >= LEVEL_VERSION_1_4 ? sizeof(float) : 0)  // prismatic_tolerance
         + (this->version >= LEVEL_VERSION_1_4 ? sizeof(float) : 0)  // pivot_tolerance

         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(uint64_t) : 0)  // seed
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(uint32_t) : 0)  // adventure_id

          // num groups, entities, connections, cables, chunks, state_size, num_gentypes
         + (this->version >= LEVEL_VERSION_1_5 ? 7 * sizeof(uint32_t) : 4 * sizeof(uint16_t))

         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(float) : 0) // linear damping
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(float) : 0) // angular damping
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(float) : 0) // joint friction
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(float) : 0)  // dead enemy absorb time
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(float) : 0)  // time before player can respawn
         + (this->version >= LEVEL_VERSION_1_5 ? sizeof(uint64_t) : 0)  // compression buffer length

         + this->name_len
         + (this->version >= 6 ? 128*128 : 0)
         + this->descr_len;
}

void lvlinfo::write(lvlbuf *lb) {
    this->sanity_check();

    lb->ensure(this->get_size());

    lb->w_uint8(this->version);
    lb->w_uint8(this->type);

    if (this->type != LCAT_PARTIAL) {
        lb->w_uint32(this->community_id);

        if (this->version >= LEVEL_VERSION_1_5) {
            lb->w_uint32(this->autosave_id);
        }

        lb->w_uint32(this->revision);
        lb->w_uint32(this->parent_id);
    }

    lb->w_uint8(this->name_len);

    if (this->type != LCAT_PARTIAL) {
        lb->w_uint16(this->descr_len);
        lb->w_uint8(1);
        if (this->version >= 3) {
            lb->w_uint8(this->visibility);
        }
        if (this->version >= 7) {
            lb->w_uint32(this->parent_revision);
            lb->w_uint8(this->pause_on_finish);
            lb->w_uint8(this->show_score);
        }
        lb->w_uint8(this->bg);
        if (this->version >= LEVEL_VERSION_1_5) {
            lb->w_uint32(this->bg_color);
        }

        if (this->version >= 12) {
            lb->w_uint16(this->size_x[0]);
            lb->w_uint16(this->size_x[1]);
            lb->w_uint16(this->size_y[0]);
            lb->w_uint16(this->size_y[1]);
        } else {
            lb->w_uint16(this->size_x[0]+this->size_x[1]);
            lb->w_uint16(this->size_y[0]+this->size_y[1]);
        }

        lb->w_uint8(this->velocity_iterations);
        lb->w_uint8(this->position_iterations);
        lb->w_uint32(this->final_score);
        lb->w_float(this->sandbox_cam_x);
        lb->w_float(this->sandbox_cam_y);
        lb->w_float(this->sandbox_cam_zoom);

        if (this->version >= 3) {
            lb->w_float(this->gravity_x);
            lb->w_float(this->gravity_y);
        }
    }

    if (this->version >= 13) {
        lb->w_float(this->min_x);
        lb->w_float(this->max_x);
        lb->w_float(this->min_y);
        lb->w_float(this->max_y);
    }

    if (this->type != LCAT_PARTIAL) {
        if (this->version >= 9) {
            lb->w_uint64(this->flags);
        }

        if (this->version >= LEVEL_VERSION_1_4) {
            lb->w_float(this->prismatic_tolerance);
            lb->w_float(this->pivot_tolerance);
        }

        if (this->version >= LEVEL_VERSION_1_5) {
            lb->w_uint64(this->seed);
            lb->w_uint32(this->adventure_id);
            lb->w_float(this->linear_damping);
            lb->w_float(this->angular_damping);
            lb->w_float(this->joint_friction);
            lb->w_float(this->dead_enemy_absorb_time);
            lb->w_float(this->time_before_player_can_respawn);
            lb->w_uint64(this->compression_length);
        }
    }

    if (this->name_len) {
        lb->w_buf(this->name, this->name_len);
    }

    if (this->type != LCAT_PARTIAL) {
        if (this->version >= 6) {
            int nn = 0;
            int n_max = 0;
            for (int y=0; y<128*128; y++) {
                if (icon[y] != 0) nn ++;
                if (icon[y] > n_max) n_max = icon[y];
            }
            lb->w_buf((const char *)&this->icon, 128*128);
        }
        if (this->descr_len)
            lb->w_buf(this->descr, this->descr_len);
    }

    if (this->version >= LEVEL_VERSION_1_5) {
        lb->w_uint32(this->num_groups);
        lb->w_uint32(this->num_entities);
        lb->w_uint32(this->num_connections);
        lb->w_uint32(this->num_cables);
        lb->w_uint32(this->num_chunks);
        lb->w_uint32(this->state_size);
        lb->w_uint32(this->num_gentypes);
    } else {
        lb->w_uint16((uint16_t)this->num_groups);
        lb->w_uint16((uint16_t)this->num_entities);
        lb->w_uint16((uint16_t)this->num_connections);
        lb->w_uint16((uint16_t)this->num_cables);
    }
}

bool lvlinfo::read(lvlbuf *lb, bool skip_description) {
    this->version = lb->r_uint8();
    if (this->version > LEVEL_VERSION) {
        return false;
    }

    this->type = lb->r_uint8();

    if (this->type != LCAT_PARTIAL) {
        this->community_id = lb->r_uint32();

        if (this->version >= LEVEL_VERSION_1_5) {
            this->autosave_id = lb->r_uint32();
        }

        this->revision = lb->r_uint32();
        this->parent_id = lb->r_uint32();
    }

    this->name_len = lb->r_uint8();

    if (this->type != LCAT_PARTIAL) {
        this->descr_len = lb->r_uint16();
        // XXX: Get rid of this value in next level version (allow_derivatives)
        (void)lb->r_uint8();
        if (this->version >= 3)
            this->visibility = lb->r_uint8();
        else
            this->visibility = LEVEL_VISIBLE;
        if (this->version >= 7) {
            this->parent_revision = lb->r_uint32();
            this->pause_on_finish = (bool)lb->r_uint8();
            this->show_score = (bool)lb->r_uint8();
        } else {
            this->parent_revision = 0;
            this->pause_on_finish = true;
            this->show_score = 1;
        }
        this->bg = lb->r_uint8();

        if (this->version >= LEVEL_VERSION_1_5) {
            this->bg_color = lb->r_uint32();
        }

        if (this->version >= 12) {
            this->size_x[0] = lb->r_uint16();
            this->size_x[1] = lb->r_uint16();
            this->size_y[0] = lb->r_uint16();
            this->size_y[1] = lb->r_uint16();
        } else {
            this->size_x[0] = lb->r_uint16();
            this->size_y[0] = lb->r_uint16();
            this->size_x[1] = this->size_x[0];
            this->size_y[1] = this->size_y[0];
        }

        this->velocity_iterations = lb->r_uint8();
        this->position_iterations = lb->r_uint8();
        this->final_score = lb->r_uint32();
        this->sandbox_cam_x = lb->r_float();
        this->sandbox_cam_y = lb->r_float();
        this->sandbox_cam_zoom = lb->r_float();

        if (this->version >= 3) {
            this->gravity_x = lb->r_float();
            this->gravity_y = lb->r_float();
        } else {
            this->gravity_x = 0.f;
            this->gravity_y = -20.f;
        }
    }

    if (this->version >= 13) {
        min_x = lb->r_float();
        max_x = lb->r_float();
        min_y = lb->r_float();
        max_y = lb->r_float();
    } else {
        min_x = 0.f;
        max_x = 0.f;
        min_y = 0.f;
        max_y = 0.f;
    }

    if (this->type != LCAT_PARTIAL) {
        if (this->version >= 9) {
            this->flags = lb->r_uint64();

            if (this->version < LEVEL_VERSION_1_5) {
                this->flags |= LVL_ALLOW_RESPAWN_WITHOUT_CHECKPOINT;
            }
        } else {
            this->flags = 0;
        }

        if (this->version >= 26) {
            this->prismatic_tolerance = lb->r_float();
            this->pivot_tolerance = lb->r_float();
        } else {
            this->prismatic_tolerance = 0.f;
            this->pivot_tolerance = 0.f;
        }

        if (this->version >= LEVEL_VERSION_1_5) {
            this->seed = lb->r_uint64();
            this->adventure_id = lb->r_uint32();
            this->linear_damping = lb->r_float();
            this->angular_damping = lb->r_float();
            this->joint_friction = lb->r_float();
            this->dead_enemy_absorb_time = lb->r_float();
            this->time_before_player_can_respawn = lb->r_float();
            this->compression_length = lb->r_uint64();
        } else {
            this->seed = 0;
            this->adventure_id = 0xffffffff;
            this->linear_damping = 0.f;
            this->angular_damping = 0.f;
            this->joint_friction = 0.f;
            this->dead_enemy_absorb_time = 10.f;
            this->time_before_player_can_respawn = 0.f;
            this->compression_length = 0;
        }
    }

    if (this->name_len) {
        lb->r_buf(this->name, this->name_len);
    }

    if (this->type != LCAT_PARTIAL) {
        if (this->version >= 6) {
            //tms_infof(" reading icon, pt: %d, end: %d", lb->rp, lb->size);
            lb->r_buf((char *)&this->icon, 128*128);
        } else
            memset(this->icon, 0, 128*128);

        if (this->descr_len && !skip_description) {
            this->descr = (char*)realloc(this->descr, this->descr_len+1);
            lb->r_buf(this->descr, this->descr_len);
            this->descr[this->descr_len] = '\0';
        }
    }

    if (this->version >= LEVEL_VERSION_1_5) {
        this->num_groups = lb->r_uint32();
        this->num_entities = lb->r_uint32();
        this->num_connections = lb->r_uint32();
        this->num_cables = lb->r_uint32();
        this->num_chunks = lb->r_uint32();
        this->state_size = lb->r_uint32();
        this->num_gentypes = lb->r_uint32();
    } else {
        this->num_groups = lb->r_uint16();
        this->num_entities = lb->r_uint16();
        this->num_connections = lb->r_uint16();
        this->num_cables = lb->r_uint16();
        this->num_chunks = 0;
        this->state_size = 0;
        this->num_gentypes = 0;
    }

    this->sanity_check();

    return true;
}

void lvlinfo::print() const {
    printf("Level headers:\n");

    printf("Level version:       %u\t(%s)\n",
            this->version, level_version_string(this->version));

    printf("Level type:          %u\t(%s)\n",
            this->type, level_type_string(this->type));

    if (this->type != LCAT_PARTIAL) {
        printf("Community ID:        %u\n",
                this->community_id);

        if (this->version >= LEVEL_VERSION_1_5) {
            printf("Autosave ID:         %u\n",
                    this->autosave_id);
        }

        printf("Revision:            %u\n",
                this->revision);

        printf("Parent ID:           %u\n",
                this->parent_id);
    }

    printf("Name len:            %u\n",
            this->name_len);

    if (this->type != LCAT_PARTIAL) {
        printf("Description len:     %u\n",
                this->descr_len);

        printf("Visibility:          %s\n",
                level_visibility_string(this->visibility));

        printf("Visibility:          %u\n",
                this->parent_revision);

        printf("Pause on finish:     %s\n",
                this->pause_on_finish ? "yes" : "no");

        printf("Show score:          %s\n",
                this->show_score ? "yes" : "no");

        printf("Background ID:       %u\n",
                this->bg);

        if (this->version >= LEVEL_VERSION_1_5) {
            printf("Background color:    %u\n",
                    this->bg_color);
        }

        printf("Border left:         %u\n",
                this->size_x[0]);

        printf("Border right:        %u\n",
                this->size_x[1]);

        printf("Border down:         %u\n",
                this->size_y[0]);

        printf("Border up:           %u\n",
                this->size_y[1]);

        printf("Vel iterations:      %u\n",
                this->velocity_iterations);

        printf("Pos iterations:      %u\n",
                this->position_iterations);

        printf("Final score:         %u\n",
                this->final_score);

        printf("Sandbox cam X:       %f\n",
                this->sandbox_cam_x);

        printf("Sandbox cam Y:       %f\n",
                this->sandbox_cam_y);

        printf("Sandbox cam Z:       %f\n",
                this->sandbox_cam_zoom);

        if (this->version >= 3) {
            printf("Gravity X:           %.6f\n",
                    this->gravity_x);
            printf("Gravity Y:           %.6f\n",
                    this->gravity_y);
        }
    }

    printf("Min/Max X:           %.2f/%.2f\n",
            this->min_x,
            this->max_x);

    printf("Min/Max Y:           %.2f/%.2f\n",
            this->min_y,
            this->max_y);

    if (this->type != LCAT_PARTIAL) {
        printf("Flags:               %" PRIu64 "\n",
                this->flags);

        printf("Prismatic tolerance: %.2f\n",
                this->prismatic_tolerance);

        printf("Pivot tolerance:     %.2f\n",
                this->pivot_tolerance);

        printf("Seed:                0x%016" PRIx64 "\n",
                this->seed);

        printf("Adventure ID:        0x%08" PRIx32 "\n",
                this->adventure_id);

        printf("Linear damping:      %.2f\n",
                this->linear_damping);

        printf("Angular damping:     %.2f\n",
                this->angular_damping);

        printf("Joint friction:      %.2f\n",
                this->joint_friction);

        printf("Enemy absorb time:   %.2f\n",
                this->dead_enemy_absorb_time);

        printf("Player respawn time: %.2f\n",
                this->time_before_player_can_respawn);
    }

    if (this->name_len) {
        printf("Name:                (%u) '%s'\n",
                (unsigned)strlen(this->name), this->name);
    }

    if (this->type != LCAT_PARTIAL) {
        if (this->descr_len) {
            printf("Description:         (%u) '%s'\n",
                    (unsigned)strlen(this->descr), this->descr);
        }
    }

    printf("Num groups:          %u\n",
            this->num_groups);

    printf("Num entities:        %u\n",
            this->num_entities);

    printf("Num connections:     %u\n",
            this->num_connections);

    printf("Num chunks:          %u\n",
            this->num_chunks);

    printf("Num gentypes:        %u\n",
            this->num_gentypes);

    printf("State size:          %u\n",
            this->state_size);

    printf("Num cables:          %u\n",
            this->num_cables);
}

// pkginfo

uint32_t pkginfo::get_next_level(uint32_t id) {
    if (this->return_on_finish) {
        if (this->first_is_menu)
            return this->levels[0];
        else
            return 0; // return to pkg screen
    }

    // find the level in the package, return the next level or 0 if it was the last level
    for (int x=0; x<this->num_levels-1; x++) {
        if (this->levels[x] == id)
            return this->levels[x+1];
    }

    return 0;
}

bool pkginfo::is_level_locked(uint8_t index) {
    uint8_t i_unlocked = 0;
    int x = 0;

    if (index >= this->num_levels) return true;
    if (this->unlock_count == 0) return false;

    if (this->first_is_menu) {
        x = 1;
        i_unlocked++;
    }

    for (; x<index; ++x) {
        lvl_progress *p = progress::get_level_progress(this->type, this->levels[x]);
        if (p->completed)
            i_unlocked++;
    }

    if ((index - i_unlocked) < this->unlock_count)
        return false;

    return true;
}

bool pkginfo::add_level(uint32_t id) {
    // make sure it isnt already added
    for (int x=0; x<this->num_levels; x++)
        if (this->levels[x] == id) return false;

    this->levels = (uint32_t*)realloc(this->levels, sizeof(uint32_t)*(this->num_levels+1));

    if (!this->levels) exit(1);

    this->levels[this->num_levels] = id;
    this->num_levels ++;

    return true;
}

bool pkginfo::open(int type, uint32_t id) {
    if (type < 0 || type >= 4) {
        tms_errorf("invalid level type");
        return false;
    }

    char path[1024];
    char *storage = (char*)pkgman::get_pkg_path(type);
    snprintf(path, 1023, "%s/%d.ppkg", storage, id);

    this->type = type;
    this->id = id;

    if (this->levels) free(this->levels);
    this->levels = 0;
    this->num_levels = 0;
    this->name[0] = '\0';
    this->name[255] = '\0';

    FILE_IN_ASSET(type == LEVEL_MAIN);

    FILE *fp = _fopen(path, "rb");

    if (!fp) {
        tms_errorf("could not open: %s", path);
        return false;
    }

    _fread(&this->version, 1, 1, fp);
    if (this->version >= 2)
        _fread(&this->community_id, 1, sizeof(uint32_t), fp);
    else
        this->community_id = 0;

    _fread(this->name, 1, 255, fp);

    if (this->version >= 3)
        _fread(&this->unlock_count, 1, sizeof(uint8_t), fp);

    if (this->version >= 1) {
        _fread(&this->first_is_menu, 1, sizeof(uint8_t), fp);
        _fread(&this->return_on_finish, 1, sizeof(uint8_t), fp);
    }

    _fread(&this->num_levels, 1, sizeof(uint8_t), fp);

    if (this->num_levels)
        this->levels = (uint32_t*)malloc(this->num_levels * sizeof(uint32_t));

    for (int x=0; x<this->num_levels; x++) {
        _fread(&this->levels[x], 1, sizeof(uint32_t), fp);
    }

    _fclose(fp);

    return true;
}

bool pkginfo::save() {
    if (this->type >= 3) {
        tms_errorf("invalid level type");
        return false;
    }

    char path[1024];
    char *storage = (char*)pkgman::get_pkg_path(this->type);
    snprintf(path, 1023, "%s/%d.ppkg", storage, this->id);

    return save_to_path(path);
}

bool pkginfo::save_to_path(const char *path) {
    FILE *fp = fopen(path, "wb");

    if (!fp) {
        tms_errorf("could not open: %s", path);
        return false;
    }

    // always update the version to the latest on save
    this->version = PKG_VERSION;

    fwrite(&this->version, 1, 1, fp);
    fwrite(&this->community_id, 1, sizeof(uint32_t), fp);
    fwrite(this->name, 1, 255, fp);
    fwrite(&this->unlock_count, 1, sizeof(uint8_t), fp);
    fwrite(&this->first_is_menu, 1, sizeof(uint8_t), fp);
    fwrite(&this->return_on_finish, 1, sizeof(uint8_t), fp);
    fwrite(&this->num_levels, 1, sizeof(uint8_t), fp);

    for (int x=0; x<this->num_levels; x++) {
        fwrite(&this->levels[x], 1, sizeof(uint32_t), fp);
    }

    fclose(fp);

    return true;
}

// pkgman

static uint32_t get_next_id(const char *storage, const char *ext) {
    char path[1024];
    struct stat s;

    // Check IDs up to 2 billion. Ought to be enough for everyone.
    for (uint32_t x = 1; x < 2e9; x++) {
        snprintf(path, 1023, "%s/%d.%s", storage, x, ext);
        int i = stat(path, &s);

        if (i == -1)
            return x;
    }

    return 0;
}

uint32_t pkgman::get_next_pkg_id() {
    return get_next_id(pkgman::get_pkg_path(LEVEL_LOCAL), "ppkg");
}

uint32_t pkgman::get_next_level_id() {
    return get_next_id(pkgman::get_level_path(LEVEL_LOCAL), "plvl");
}

uint32_t pkgman::get_next_object_id() {
    return get_next_id(pkgman::get_level_path(LEVEL_LOCAL), "pobj");
}

const char *pkgman::get_pkg_path(int type) {
    if (type < 0 || type >= 4) {
        tms_errorf("invalid level type");
        return "";
    }

    if (!_pkg_path[type][0]) {
        if (type == LEVEL_MAIN)
            // main levels are stored internally in data/
            snprintf(_pkg_path[type], 1023, "data/pkg");
        else if (type == LEVEL_DB)
            snprintf(_pkg_path[type], 1023, "%s/pkg/db", tms_storage_cache_path());
        else
            snprintf(_pkg_path[type], 1023, "%s/packages", tms_storage_path());
    }

    return _pkg_path[type];
}

const char *pkgman::get_level_ext(int level_type) {
    if (level_type == LEVEL_PARTIAL) return "pobj";
    else if (level_type >= LEVEL_LOCAL_STATE) return "psav";
    else return "plvl";
}

const char *pkgman::get_level_path(int level_type) {
    if (level_type == LEVEL_PARTIAL) level_type = LEVEL_LOCAL;

    if (level_type >= LEVEL_LOCAL_STATE) {
        if (!_state_path[0])
            snprintf(_state_path, 1023, "%s/saves", tms_storage_path());

        return _state_path;
    }

    if (level_type < 0 || level_type >= 4) {
        tms_errorf("invalid level type");
        return "";
    }

    if (!_level_path[level_type][0]) {
        if (level_type == LEVEL_MAIN)
            // main levels are stored internally in data/
            snprintf(_level_path[level_type], 1023, "data/lvl");
        else if (level_type == LEVEL_SYS)
            snprintf(_level_path[level_type], 1023, "%s/cache/local", tms_storage_cache_path());
        else if (level_type == LEVEL_DB)
            snprintf(_level_path[level_type], 1023, "%s/lvl/db", tms_storage_cache_path());
        else
            snprintf(_level_path[level_type], 1023, "%s/levels", tms_storage_path());
    }

    return _level_path[level_type];
}

const char *pkgman::get_state_prefix(int level_type) {
    if (level_type >= LEVEL_LOCAL_STATE)
        level_type -= LEVEL_LOCAL_STATE;

    if (level_type == LEVEL_LOCAL)
        return "local";
    else if (level_type == LEVEL_DB)
        return "db";

    tms_errorf("Unknown state prefix for level type %d", level_type);
    return "unknown";
}

const char *pkgman::get_cache_path(int level_type) {
    if (level_type == LEVEL_PARTIAL) level_type = LEVEL_LOCAL;

    if (level_type >= LEVEL_LOCAL_STATE) {
        if (!_cache_state_path[0]) {
            snprintf(_cache_state_path, 1023,
                    "%s/cache/sav",
                    tms_storage_cache_path());
        }

        return _cache_state_path;
    }

    if (level_type < 0 || level_type >= 4) {
        tms_errorf("invalid level type");
        return "";
    }

    if (!_cache_path[level_type][0]) {
        snprintf(_cache_path[level_type], 1023,
                "%s/cache/%s",
                tms_storage_cache_path(),
                _dir_names[level_type]);
    }

    return _cache_path[level_type];
}

void pkgman::get_level_full_path(int level_type, uint32_t id, uint32_t save_id, char *output) {
    if (level_type >= LEVEL_LOCAL_STATE) {
        snprintf(output, 1023, "%s/%s.%d.%u.%s",
                pkgman::get_level_path(level_type),
                pkgman::get_state_prefix(level_type),
                id,
                save_id,
                pkgman::get_level_ext(level_type));
    } else {
        snprintf(output, 1023, "%s/%d.%s",
                pkgman::get_level_path(level_type),
                id,
                pkgman::get_level_ext(level_type));
    }
}

void pkgman::get_cache_full_path(int level_type, uint32_t id, uint32_t save_id, char *output) {
    if (level_type >= LEVEL_LOCAL_STATE) {
        snprintf(output, 1023, "%s/%s.%u.%u.pcache",
                pkgman::get_cache_path(level_type),
                pkgman::get_state_prefix(level_type),
                id,
                save_id);
    } else {
        snprintf(output, 1023, "%s/%d.pcache",
                pkgman::get_cache_path(level_type),
                id);
    }
}

bool pkgman::get_level_name(int level_type, uint32_t id, uint32_t save_id, char *output) {
    uint8_t o_version;
    pkgman::get_level_data(level_type, id, save_id, output, &o_version);
    return true;
}

bool pkgman::get_level_data(int level_type, uint32_t id, uint32_t save_id, char *o_name, uint8_t *o_version) {
    char filename[1024];

    pkgman::get_level_full_path(level_type, id, save_id, filename);

    FILE *fp = fopen(filename, "rb");

    if (!fp) {
        tms_warnf("unable to open file for lid %u", id);
        return false;
    }

    tmpbuf.reset();
    tmpbuf.ensure(sizeof(lvlinfo));

    fread(tmpbuf.buf, 1, sizeof(lvlinfo), fp);
    tmpbuf.size = sizeof(lvlinfo);

    tmplvl.read(&tmpbuf, true);

    if (tmplvl.name_len == 0) {
        strcpy(o_name, "<no name>");
    } else {
        memcpy(o_name, tmplvl.name, tmplvl.name_len*sizeof(char));
        o_name[tmplvl.name_len] = '\0';
    }

    *o_version = tmplvl.version;

    fclose(fp);

    return true;
}

pkginfo *pkgman::get_pkgs(int type) {
    if (type < 0 || type >= 4)
        return 0;

    const char *path = pkgman::get_pkg_path(type);

    DIR *dir;
    struct dirent *ent;
    pkginfo *first = 0, *curr = 0;

    if ((dir = opendir(path))) {

        while ((ent = readdir(dir))) {
            int len = strlen(ent->d_name);
            if (len > 5 && memcmp(&ent->d_name[len-5], ".ppkg", 5) == 0) {
                uint32_t pkg_id = atoi(ent->d_name);

                if (pkg_id != 0) {
                    pkginfo *ff = new pkginfo();

                    if (ff->open(type, pkg_id)) {
                        if (curr) curr->next = ff;
                        if (!first) first = ff;
                        curr = ff;
                    } else {
                        tms_errorf("could not open pkg %d", pkg_id);
                        delete ff;
                    }
                }
            }
        }
        closedir(dir);
    }

    return first;
}

lvlfile *pkgman::get_levels(int level_type) {
    bool state = false;

    int orig_level_type = level_type;

    if (level_type >= LEVEL_LOCAL_STATE) {
        level_type -= LEVEL_LOCAL_STATE;
        state = true;
    }

    if (level_type < 0 || level_type >= 5) {
        tms_warnf("unknown level type");
        return 0;
    }

    char ext[6];
    snprintf(ext, 6, ".%s", pkgman::get_level_ext(orig_level_type));
    const char *path = pkgman::get_level_path(orig_level_type);

    char tmp[1024];

    DIR *dir;
    struct dirent *ent;
    lvlfile *first = 0, *last = 0;

    dir = opendir(path);

    if (!dir) {
        tms_errorf("could not open directory %s", path);
        return first;
    }

    while ((ent = readdir(dir))) {
        int len = strlen(ent->d_name);

        if (!(len > 5 && memcmp(&ent->d_name[len-5], ext, 5) == 0))
            continue;

        uint32_t level_id = 0;
        uint32_t save_id = 0;
        uint8_t state_level_type = LEVEL_LOCAL_STATE;

        if (state) {
            // State format: {0:s:type}.{1:d:id}.{2:d:save_id}.{3:s:ext}
            if (ent->d_name[0] == 'l')
                state_level_type = LEVEL_LOCAL_STATE;
            else if (ent->d_name[0] == 'd')
                state_level_type = LEVEL_DB_STATE;

            char *level_id_c = strchr(ent->d_name, '.');
            level_id = atoi(level_id_c+1);
            save_id = atoi(strchr(level_id_c+1, '.')+1);
        } else {
            // Regular level format: {0:d:id}.{1:s:save_id}
            level_id = atoi(ent->d_name);

            save_id = atoi(strchr(ent->d_name, '.')+1);
        }

        snprintf(tmp, 1023, "%s/%s", path, ent->d_name);
        time_t mtime = get_mtime(tmp);

        char date[21];
        strftime(date, 20, "%Y-%m-%d %H:%M:%S", gmtime((time_t*)&(mtime)));

        if (!(level_id != 0 || state))
            continue;

        lvlfile *ff = new lvlfile((state ? state_level_type : level_type), level_id);
        strcpy(ff->modified_date, date);
        ff->mtime = mtime;
        ff->save_id = save_id;

        /**
         * For state searches, we do not care about the original level type.
         * The original level type is merely there to notify us that it's states
         * we are looking for.
         * The level type of the current state is discerned via the filename
         * using the first "dotted" argument (level or db).
         **/
        if (!pkgman::get_level_data((state ? state_level_type : orig_level_type), level_id, save_id, ff->name, &ff->version)) {
            tms_warnf("Unable to get level name for lid %u", level_id);
            delete ff;
            continue;
        }

        if (!first) {
            first = ff;
        } else {
            // loop through and insert the lvlfile as soon as the date is newer

            lvlfile *l = first, *prev = 0;
            while (l) {
                if (strcmp(ff->modified_date, l->modified_date) > 0) {
                    break;
                }
                prev = l;
                l = l->next;
            }

            if (!prev) {
                ff->next = first;
                first = ff;
            } else {
                ff->next = prev->next;
                prev->next = ff;
            }
        }
    }
    closedir(dir);

    return first;
}

uint32_t pkgman::get_latest_level_id(int level_type) {
    lvlfile *level = pkgman::get_levels(LEVEL_LOCAL);

    uint32_t latest_id = 0;
    time_t latest_mtime = 0;

    while (level) {
        if (level->mtime > latest_mtime) {
            latest_id = level->id;
            latest_mtime = level->mtime;
        }

        lvlfile *next = level->next;
        delete level;
        level = next;
    }

    return latest_id;
}

extern "C" struct lvlfile* pkgman_get_levels(int level_type) {
    return pkgman::get_levels(level_type);
}

// lvledit

bool lvledit::open(int lvl_type, uint32_t lvl_id) {
    char filename[1024];

    const char *ext = pkgman::get_level_ext(lvl_type);
    const char *path = pkgman::get_level_path(lvl_type);

    if (lvl_id == 0)
        snprintf(filename, 1024, "%s/.autosave", path);
    else
        snprintf(filename, 1024, "%s/%d.%s", path, lvl_id, ext);

    this->lvl_type = 0;
    this->lvl_id = 0;

    if (!open_from_path(filename)) {
        tms_errorf("could not open level file '%s'", filename);
        return false;
    }

    this->lvl_type = lvl_type;
    this->lvl_id = lvl_id;

    return true;
}

bool lvledit::save() {
    char filename[1024];

    const char *ext = pkgman::get_level_ext(lvl_type);
    const char *path = pkgman::get_level_path(lvl_type);

    snprintf(filename, 1024, "%s/%d.%s", path, this->lvl_id, ext);

    return save_to_path(filename);
}

bool lvledit::open_from_path(const char *path) {
    FILE *fp = fopen(path, "rb");

    if (!fp) {
        tms_errorf("could not open file '%s'", path);
        return false;
    }

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    if (size > 2*1024*1024) {
        tms_fatalf("file too big");
    }

    this->lb.reset();
    this->lb.size = 0;
    this->lb.ensure((int)size);

    fread(this->lb.buf, 1, size, fp);

    fclose(fp);

    this->lb.size = size;
    this->lvl.read(&this->lb);
    this->header_size = this->lvl.get_size();

    return true;
}

bool lvledit::save_to_path(const char *path) {
    if (this->lvl.get_size() != this->header_size) {
        // new header size does not match old header size,
        // we need to perform a memmove on the object data

        int diff = this->lvl.get_size() - this->header_size;

        if (diff > 0)
            this->lb.ensure(diff);

        char *header_end = (char*)this->lb.buf + this->header_size;
        memmove(header_end + diff, header_end, this->lb.size - this->header_size);

        this->header_size += diff;
        this->lb.size += diff;
    }

    int saved_size = this->lb.size;
    this->lb.size = 0;
    this->lvl.write(&this->lb);
    this->lb.size = saved_size;

    FILE *fp = fopen(path, "wb");

    if (!fp) {
        tms_errorf("could not open file '%s' for writing", path);
        return false;
    }

    fwrite(this->lb.buf, 1, this->lb.size, fp);
    fclose(fp);

    return true;
}

void lvledit::print_gids() {
    // IMPORTANT: keep this in sync with of::read()
    this->lb.rp = this->lvl.get_size();

    uint32_t num_groups = this->lvl.num_groups;
    uint32_t num_entities = this->lvl.num_entities;

    // skip groups
#if 0
    this->lb.rp +=
        num_groups *
        (
          sizeof(uint32_t) // id
          +sizeof(float) // pos.x
          +sizeof(float) // pos.y
          +sizeof(float) // angle
          +(version >= LEVEL_VERSION_1_5 ? sizeof(uint32_t) : 0)
        );
#endif
    for (uint32_t x = 0; x < num_groups; ++x) {
        lb.r_uint32();  // id
        lb.rp += sizeof(float) // pos.x
               + sizeof(float) // pos.y
               + sizeof(float);// angle

        if (this->lvl.version >= LEVEL_VERSION_1_5) {
            uint32_t state_size = lb.r_uint32();
            lb.rp += state_size;
        }
    }

    for (uint32_t x = 0; x < num_entities; ++x) {
        uint8_t np;
        uint8_t nc = 0;
        uint32_t state_size = 0;

        uint8_t g_id = lb.r_uint8(); // g_id
        lb.r_uint32(); // id
        lb.r_uint16(); // group_id half
        lb.r_uint16(); // group id half

        printf("%s%u",((x>0)?"\n":""), g_id); // g_id

        np = lb.r_uint8(); // num properties

        if (this->lvl.version >= LEVEL_VERSION_1_5) {
            nc = lb.r_uint8(); // num chunks
            state_size = lb.r_uint32(); // state size
        }

        (void)lb.r_float(); // pos.x
        (void)lb.r_float(); // pos.y
        (void)lb.r_float(); // angle
        (void)lb.r_uint8(); // layer

        if (this->lvl.version >= LEVEL_VERSION_1_5) {
            lb.r_uint64(); // state flags
            // this includes moveable and axisrot

            for (int x=0; x<nc; x++) {
                lb.r_int32(); // chunk x
                lb.r_int32(); // chunk y
            }

            // skip state buffer
            lb.rp += state_size;
        } else {
            (void)lb.r_uint8(); // axisrot
            if (this->lvl.version >= 10) {
                (void)lb.r_uint8(); // moveable
            }
        }

        for (int x=0; x<np; x++) {
            uint8_t type = lb.r_uint8();

            switch (type) {
                case P_INT8:
                    lb.r_uint8();
                    break;

                case P_INT:
                case P_ID:
                    lb.r_uint32();
                    break;

                case P_FLT:
                    lb.r_float();
                    break;

                case P_STR:
                    if (this->lvl.version >= LEVEL_VERSION_1_5) {
                        uint32_t len = lb.r_uint32();
                        lb.rp += len;
                    } else {
                        uint16_t len = lb.r_uint16();
                        lb.rp += len;
                    }
                break;

                default: fprintf(stderr, "invalid object property %d", type); exit(1);
            }
        }
    }
}

// lvlbuf

void lvlbuf::ensure(uint64_t s) {
    uint64_t ns = this->size + s;
    if (ns > this->cap) {
        uint64_t old_cap = this->cap;
        if (this->sparse_resize) {
            // round up to nearest power of two
            this->cap = ns;
            this->cap --;
            this->cap |= this->cap >> 1;
            this->cap |= this->cap >> 2;
            this->cap |= this->cap >> 4;
            this->cap |= this->cap >> 8;
            this->cap |= this->cap >> 16;
            this->cap++;
        } else {
            this->cap = ((ns > this->cap+4096) ? ns+4096 : this->cap+4096);
        }

        this->buf = static_cast<uint8_t*>(realloc(this->buf, this->cap));
    }
}

void lvlbuf::zcompress(const lvlinfo &level, unsigned char **dest, uint64_t *dest_len) const {
    uint64_t header_size = level.get_size();

    *dest_len = compressBound(this->size-header_size);
    *dest = (unsigned char*)malloc(*dest_len);

    int ret = compress(*dest, (uLong*)dest_len, this->buf+header_size, this->size-header_size);

    // TODO: Better error-handling
    switch (ret) {
        case Z_OK:
            tms_infof("zcompress success");
            break;

        case Z_MEM_ERROR:
            tms_errorf("no mem");
            break;

        case Z_BUF_ERROR:
            tms_errorf("not enough room in buffer");
            break;

        case Z_STREAM_ERROR:
            tms_errorf("z stream error");
            break;

        default:
            tms_infof("unknown ret: %d", ret);
            break;
    }

    tms_infof("Compression status: Old: %" PRIu64 ". New: %" PRIu64 ". Total: %.2f", this->size-header_size, *dest_len, (float)(float)*dest_len/((this->size-header_size)));
}

void lvlbuf::zuncompress(const lvlinfo &level) {
    uint64_t header_size = level.get_size();

    uLong dest_len = level.compression_length;
    unsigned char *dest = (unsigned char*)malloc(dest_len);

    int ret = uncompress(dest, &dest_len, this->buf+header_size, this->size-header_size);

    // TODO: Better error-handling
    switch (ret) {
        case Z_OK:
            tms_infof("zuncompress success");
            break;

        case Z_MEM_ERROR:
            tms_errorf("no mem");
            break;

        case Z_BUF_ERROR:
            tms_errorf("not enough room in buffer");
            break;

        case Z_STREAM_ERROR:
            tms_errorf("z stream error");
            break;

        default:
            tms_infof("unknown ret: %d", ret);
            break;
    }

    tms_infof("Uncompression status: Old: %" SDL_PRIu64 ". New: %u. Total: %.2f", this->size-header_size, (unsigned)dest_len, (float)(float)dest_len/((this->size-header_size)));

    this->size = header_size;
    this->ensure(dest_len);
    this->size = header_size + dest_len;

    memcpy(this->buf+header_size, dest, dest_len);

    free(dest);
}
