/*
 * Copyright (C) 2012 David Anderson
 *
 * This file is part of SourcePawn.
 *
 * SourcePawn is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 * 
 * SourcePawn is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * SourcePawn. If not, see http://www.gnu.org/licenses/.
 */
#include "Zone.h"
#include "Heap.h"
#include "Strings.h"
#include "Modules.h"
#include "Packages.h"
#include "FileSystem.h"
#include "List.h"
#include "Heap-inl.h"
#include "Handles-inl.h"

using namespace ke;

Package *
Package::NewNormalized(Zone *zone, Handle<Package> parent, Handle<String> path, Handle<String> name, unsigned flags)
{
    Local<Package> package(zone, Package::cast(zone->allocate(MapKind_Package, sizeof(Package), Heap::Tenure_Old)));
    if (!package)
        return NULL;

    // :TODO: assert path, name are atoms.

    package->flags_ = flags;
    package->name_ = name;
    package->parent_ = parent;
    package->path_ = path;
    package->nativeIdentity_ = parent ? parent->getNativeIdentity() : NULL;

    return package;
}

Package *
Package::New(Zone *zone, Handle<Package> parent, const char *path, Handle<String> name, unsigned flags)
{
    Local<String> strpath(zone, zone->makeSymbol(path));
    if (!strpath)
        return NULL;

    return Package::NewNormalized(zone, parent, strpath, name, flags);
}

bool
Package::add(Zone *zone, Handle<Importable> obj)
{
    Local<Package> pkg(zone, this);

    assert(obj->parent() == pkg);

    if (!pkg->contents_) {
        pkg->contents_ = List<Importable>::New(zone);
        if (!pkg->contents_)
            return false;
    }

    if (!pkg->contents_->append(zone, obj))
        return false;

    return true;
}

Importable *
Package::import(Handle<String> name)
{
    if (!contents_)
        return NULL;
    
    for (unsigned i = 0; i < contents_->length(); i++) {
        Importable *obj = contents_->at(i);
        if (obj->isPackage() && Package::cast(obj)->name() == name)
            return obj;
        if (obj->isModule() && Module::cast(obj)->name() == name)
            return obj;
    }

    return NULL;
}

PackageCache::PackageCache(Zone *zone)
  : zone_(zone)
{
}

bool
PackageCache::initialize()
{
    return true;
}

void
PackageCache::accept(VirtualObjectVisitor *visitor)
{
    for (size_t i = 0; i < roots_.length(); i++)
        visitor->visit(roots_[i].address());
}

bool
PackageCache::checkfs(Handle<Package> package, PathComponent *path, Importable **out)
{
    AutoFree<char> left(strdup(package->path()->chars()));
    AutoFree<char> right(strdup(path->name->chars()));
    AutoFree<char> result;

    if (!JoinPaths(zone_, result, *left, *right))
        return false;

    OS::PathType type = OS::InspectPath(*result);
    if (type == OS::Path_Folder) {
        // If there is a folder matching this path, create a new subpackage,
        // and then either return it (if this is the last link), or keep
        // trying to import the rest of the path.
        Local<String> strpath(zone_, zone_->makeSymbol(*result));
        Local<Package> subpackage(zone_, Package::NewNormalized(zone_, package, strpath, path->name, Package::Normal));
        if (!subpackage)
            return false;
        if (!path->next) {
            *out = subpackage;
            return true;
        }
        return import(subpackage, path->next, out);
    }

    // Otherwise, look for a file ending in ".sp".
    AutoFree<char> filepath((char *)zone_->malloc(strlen(*result) + strlen(".sp") + 1));
    if (!*filepath)
        return false;

    strcpy(*filepath, *result);
    strcat(*filepath, ".sp");

    type = OS::InspectPath(*filepath);
    if (type != OS::Path_File)
        return true;

    // Create a new, empty module.
    Local<String> strpath(zone_, zone_->makeSymbol(*filepath));
    if (!strpath)
        return false;

    Local<Module> module(zone_, Module::New(zone_, package, strpath, path->name));
    if (!module || !package->add(zone_, module))
        return false;

    *out = module;
    return true;
}

bool
PackageCache::import(Handle<Package> package, PathComponent *path, Importable **out)
{
    // If the package is jailed, none of its imports are visible.
    if (package->jailed())
        return true;

    Local<Importable> obj(zone_, package->import(path->name));
    if (!obj) {
        // We couldn't find anything. Try the file system.
        if (!checkfs(package, path, out))
            return false;

        // checkfs will keep trying to import, so if it fails, we just
        // propagate up.
        return true;
    }

    // If we found an existing module, or the path has no next link, then
    // we can't go any further.
    if (obj->is(MapKind_Module) || !path->next) {
        *out = obj;
        return true;
    }

    Local<Package> subpackage(zone_, Package::cast(obj));
    return import(subpackage, path->next, out);
}

bool
PackageCache::findOrAdd(Handle<Package> package, PathComponent *path, Importable **out)
{
    // Local rooters should have initialized this, but whatever.
    assert(*out == NULL);

    // Try to import locally first.
    if (!import(package, path, out))
        return false;
    if (*out)
        return true;

    // Otherwise, search system roots.
    Local<Package> root(zone_);
    for (size_t i = 0; i < roots_.length(); i++) {
        root = roots_[i];
        if (!import(root, path, out))
            return false;
        if (*out)
            return true;
    }

    return true;
}

PackageCache::~PackageCache()
{
}
