
import maybe.*;
import io.paths.*;
import clay.common.*;
import clay.lexer.*;
import clay.ast as ast;
import clay.parser.*;
import clay.core.*;
import clay.loader.platform as platform;



//
// module search path
//

private var searchPath = Vector[String]();

addSearchPath(p) {
    push(searchPath, p);
}

initSearchPath() {
    var clayDir, clayExe = ..splitPath(pathToExecutable());
    var libClayDir1 = joinPath(clayDir, "lib-clay");
    var libClayDir2 = joinPath(clayDir, "..", "lib-clay");
    addSearchPath(libClayDir1);
    addSearchPath(libClayDir2);
    addSearchPath(String("."));
}



//
// locateModule
//

private var moduleSuffixes = initModuleSuffixes();

private initModuleSuffixes() {
    var os, osGroup, cpu, bits =
        platform.os, platform.osGroup, platform.cpu, platform.bits;
    var suffixes = Vector[String]();
    eachValue(
        parts -> {
            var suffix = String();
            eachValue(
                part -> {
                    printTo(suffix, '.');
                    printTo(suffix, part);
                },
                ..unpackTuple(parts),
                "clay",
            );
            push(suffixes, move(suffix));
        },
        Tuple(os, cpu, bits),
        Tuple(os, cpu),
        Tuple(os, bits),
        Tuple(cpu, bits),
        Tuple(os),
        Tuple(cpu),
        Tuple(bits),
        Tuple(osGroup),
        Tuple(),
    );
    return move(suffixes);
}

private dottedNameParts(x) = mapped(p => ref p.str, x.parts);
private moduleString(dottedName) = join(String("."), dottedNameParts(dottedName));
private moduleRelativePath(dottedName) = joinPath(dottedNameParts(dottedName));

private baseFileName(dottedName)
    = joinPath(moduleRelativePath(dottedName), back(dottedName.parts).str);

private locateModule(dottedName) {
    var base = baseFileName(dottedName);
    var relpaths = map(s -> base+s, moduleSuffixes);
    for (dir in searchPath) {
        for (relpath in relpaths) {
            var path = joinPath(dir, relpath);
            if (pathExists?(path))
                return path;
        }
    }
    throw ModuleNotFoundError(dottedName);
}



//
// addPrivateGlobal, addPublicGlobal
//

private globalVisibility(x:Maybe[ast.Visibility]) =
    maybe(x, x => x, () => ast.PUBLIC);

private addGlobal(module, visibility, name:ast.Identifier, x) {
    addGlobal(module, visibility, name, SymbolTableEntry(x));
}

overload addGlobal(module, visibility, name:ast.Identifier, x:SymbolTableEntry) {
    switch (globalVisibility(visibility))
    case (ast.PRIVATE)
        addPrivateGlobal(module, name, x);
    case (ast.PUBLIC)
        addPublicGlobal(module, name, x);
    else
        assert(false);
}

private addPrivateGlobal(module, name:ast.Identifier, x:SymbolTableEntry) {
    if (contains?(module.allGlobals.entries, name.str))
        throw GlobalRedefinitionError(name);
    module.allGlobals.entries[name.str] = x;
}

[T | VariantMember?(SymbolTableEntry, T)]
private addPublicGlobal(module, name:ast.Identifier, forward x:T) {
    addPublicGlobal(module, name, SymbolTableEntry(x));
}

overload addPublicGlobal(module, name:ast.Identifier, x:SymbolTableEntry) {
    addPrivateGlobal(module, name, x);
    module.publicGlobals.entries[name.str] = x;
}



//
// initializeSymbolTables
//

private initializeSymbolTables(module) {
    module.publicGlobals = newSymbolTable();
    module.allGlobals = newSymbolTable();
    for (x in module.ast.topLevelItems)
        installItem(module, *x);
}

private define installItem;

overload installItem(module, x:ast.Record) {
    addGlobal(module, x.visibility, x.name, Record(x));
}

overload installItem(module, x:ast.Variant) {
    addGlobal(module, x.visibility, x.name, Variant(x));
}

overload installItem(module, x:ast.Instance) {
}

overload installItem(module, x:ast.Procedure) {
    addGlobal(module, x.visibility, x.name, Procedure(x));
}

overload installItem(module, x:ast.Overload) {
}

overload installItem(module, x:ast.Enum) {
    var e = Enum(x);
    addGlobal(module, x.visibility, x.name, e);
    for (i, member in enumerated(x.members))
        addGlobal(module, x.visibility, member, EnumMember(e, i));
}

overload installItem(module, x:ast.GlobalVariable) {
    addGlobal(module, x.visibility, x.name, GlobalVariable(x));
}

overload installItem(module, x:ast.GlobalAlias) {
    addGlobal(module, x.visibility, x.name, GlobalAlias(x));
}

overload installItem(module, x:ast.ExternalProcedure) {
    addGlobal(module, x.visibility, x.name, ExternalProcedure(x));
}

overload installItem(module, x:ast.ExternalVariable) {
    addGlobal(module, x.visibility, x.name, ExternalVariable(x));
}



//
// initializePrimitivesModule
//

private initializePrimitivesModule(module) {
    module.publicGlobals = newSymbolTable();
    module.allGlobals = newSymbolTable();

    var simpleType = (name, type) => {
        addPublicGlobal(module, ast.Identifier(name), SymbolTableEntry(type));
    };
    simpleType("Bool", boolType);
    simpleType("Int8", int8Type);
    simpleType("Int16", int16Type);
    simpleType("Int32", int32Type);
    simpleType("Int64", int64Type);
    simpleType("UInt8", uint8Type);
    simpleType("UInt16", uint16Type);
    simpleType("UInt32", uint32Type);
    simpleType("UInt64", uint64Type);
    simpleType("Float32", float32Type);
    simpleType("Float64", float64Type);
    simpleType("Float80", float80Type);
    simpleType("Imag32", imag32Type);
    simpleType("Imag64", imag64Type);
    simpleType("Imag80", imag80Type);
    simpleType("Complex32", complex32Type);
    simpleType("Complex64", complex64Type);
    simpleType("Complex80", complex80Type);

    installPrimitives(module, PrimitiveTypeConstructor, PrimitiveTypeConstructorKind);
    installPrimitives(module, StaticPrimitive, StaticPrimitiveKind);
    installPrimitives(module, Primitive, PrimitiveKind);
}

private installPrimitives(module, Entry, KindEnum) {
    for (i in range(EnumMemberCount(KindEnum))) {
        var kind = KindEnum(i);
        var name = ast.Identifier(primitiveName(kind));
        var x = Entry(kind);
        addPublicGlobal(module, name, SymbolTableEntry(x));
    }
}

private primitiveName(kind) {
    var s = str(kind);
    assert(beginsWith?(s, "PRIM_"));
    return sliceFrom(s, size("PRIM_"));
}



//
// loadModule, loadPrelude, loadProgram
//

loadModule(dottedName) {
    var name = moduleString(dottedName);
    return maybe(lookupModule(name),
        module -> module,
        () -> {
            if (name == "__primitives__") {
                println("loading module: ", name);
                var module = Module(name);
                installModule(name, module);
                initializePrimitivesModule(module);
                return module;
            }
            var path = locateModule(dottedName);
            println("loading module: ", name, ", ", path);
            var module = Module(name);
            module.ast = parse(tokenize(readSource(path)));
            installModule(name, module);
            withCurrentModule(module, () => {
                initializeSymbolTables(module);
                loadDependents(module);
            });
            return module;
        },
    );
}

loadPrelude() =
    loadModule(ast.DottedName("prelude"));

loadProgram(path) {
    println("loading program: ", path);
    var module = Module(String("__main__"));
    module.ast = parse(tokenize(readSource(path)));
    withCurrentModule(module, () => {
        initializeSymbolTables(module);
        loadDependents(module);
    });
    return module;
}

private loadDependents(module) {
    for (x in module.ast.imports)
        loadModule(fieldRef(*x, #name));
}
