-- This file defines the environment in which Nomsu code runs, including some
-- basic bootstrapping functionality.
{:NomsuCode, :LuaCode, :Source} = require "code_obj"
{:Importer, :import_to_1_from, :_1_forked} = require 'importer'
{:List, :Dict, :Text} = require 'containers'
SyntaxTree = require "syntax_tree"
Files = require "files"
make_parser = require("parser")
pretty_error = require("pretty_errors")

make_tree = (tree, userdata)->
    tree.source = Source(userdata.filename, tree.start, tree.stop)
    tree.start, tree.stop = nil, nil
    tree = SyntaxTree(tree)
    return tree

Parsers = {}
max_parser_version = 0
for version=1,999
    local peg_file
    if package.nomsupath
        for path in package.nomsupath\gmatch("[^;]+")
            continue if path == "." and package.nomsupath != "."
            peg_file = io.open(path.."/nomsu.#{version}.peg")
            break if peg_file
    else
        peg_file = io.open("nomsu.#{version}.peg")
    break unless peg_file
    max_parser_version = version
    peg_contents = peg_file\read('*a')
    peg_file\close!
    Parsers[version] = make_parser(peg_contents, make_tree)

{:tree_to_nomsu, :tree_to_inline_nomsu} = require "nomsu_decompiler"
{:compile, :compile_error} = require('nomsu_compiler')
_currently_running_files = List{} -- Used to check for circular imports in run_file_1_in
nomsu_environment = Importer{
    NOMSU_COMPILER_VERSION: 12, NOMSU_SYNTAX_VERSION: max_parser_version
    -- Lua stuff:
    :next, unpack: unpack or table.unpack, :setmetatable, :rawequal, :getmetatable, :pcall,
    yield:coroutine.yield, resume:coroutine.resume, coroutine_status_of:coroutine.status,
    coroutine_wrap:coroutine.wrap, coroutine_from: coroutine.create,
    :error, :package, :os, :require, :tonumber, :tostring, :string, :xpcall, :module,
    say:print, :loadfile, :rawset, :_VERSION, :collectgarbage, :rawget, :rawlen,
    :table, :assert, :dofile, :loadstring, lua_type_of:type, :select, :math, :io, :load,
    :pairs, :ipairs, :jit, :_VERSION
    bit: (jit or _VERSION == "Lua 5.2") and require('bitops') or nil
    -- Nomsu types:
    List:List, Dict:Dict,
    -- Utilities and misc.
    lpeg:lpeg, re:re, Files:Files,
    :SyntaxTree, TESTS: Dict({}), globals: Dict({}),
    :LuaCode, :NomsuCode, :Source
    LuaCode_from: ((src, ...)-> LuaCode\from(src, ...)),
    NomsuCode_from: ((src, ...)-> NomsuCode\from(src, ...)),
    SOURCE_MAP: Importer({})

    -- Nomsu functions:
    _1_as_nomsu:tree_to_nomsu, _1_as_inline_nomsu:tree_to_inline_nomsu,
    compile: compile, _1_as_lua: compile, compile_error_at:compile_error,
    :_1_forked, :import_to_1_from,

    _1_parsed: (nomsu_code, syntax_version)->
        if type(nomsu_code) == 'string'
            filename = Files.spoof(nomsu_code)
            nomsu_code = NomsuCode\from(Source(filename, 1, #nomsu_code), nomsu_code)
        source = nomsu_code.source
        nomsu_code = tostring(nomsu_code)
        version = nomsu_code\match("^#![^\n]*nomsu[ ]+-V[ ]*([0-9.]+)")
        if syntax_version
            syntax_version = tonumber(syntax_version\match("^[0-9]+"))
        syntax_version or= version and tonumber(version\match("^[0-9]+")) or max_parser_version
        parse = Parsers[syntax_version] or Parsers[max_parser_version]
        tree = parse(nomsu_code, source.filename)
        if tree.shebang
            tree.version or= tree.shebang\match("nomsu %-V[ ]*([%d.]*)")
        errs = {}
        find_errors = (t)->
            if t.type == "Error"
                errs[#errs+1] = t
            else
                for k,v in pairs(t)
                    continue unless SyntaxTree\is_instance(v)
                    find_errors(v)
        find_errors(tree)
        num_errs = #errs
        if num_errs > 0
            err_strings = [pretty_error{
                    title:"Parse error"
                    error:e.error, hint:e.hint, source:e\get_source_file!
                    start:e.source.start, stop:e.source.stop, filename:e.source.filename
                } for i, e in ipairs(errs) when i <= 3]
            if num_errs > #err_strings
                table.insert(err_strings, "\027[31;1m +#{num_errs-#err_strings} additional errors...\027[0m\n")
            error(table.concat(err_strings, '\n\n'), 0)
        
        return tree

    run_1_in: (to_run, environment)->
        if type(to_run) == 'string'
            filename = Files.spoof(to_run)
            to_run = NomsuCode\from(Source(filename, 1, #to_run), to_run)
            ret = environment.run_1_in(to_run, environment)
            return ret
        elseif NomsuCode\is_instance(to_run)
            tree = environment._1_parsed(to_run)
            return nil if tree == nil
            ret = environment.run_1_in(tree, environment)
            return ret
        elseif SyntaxTree\is_instance(to_run)
            filename = to_run.source.filename\gsub("\n.*", "...")
            if to_run.type != "FileChunks"
                to_run = {to_run}
            -- Each chunk's compilation is affected by the code in the previous chunks
            -- (typically), so each chunk needs to compile and run before the next one
            -- compiles.
            ret = nil
            for chunk_no, chunk in ipairs to_run
                lua = environment.compile(chunk)
                lua\declare_locals!
                lua\prepend "-- File: #{filename} chunk ##{chunk_no}\n"
                ret = environment.run_1_in(lua, environment)
            return ret
        elseif LuaCode\is_instance(to_run)
            source = to_run.source
            lua_string = to_run\text!
            -- If you replace tostring(source) with "nil", source mapping won't happen
            run_lua_fn, err = load(lua_string, tostring(source), "t", environment)
            if not run_lua_fn
                lines =[("%3d|%s")\format(i,line) for i, line in ipairs Files.get_lines(lua_string)]
                line_numbered_lua = table.concat(lines, "\n")
                error("Failed to compile generated code:\n\027[1;34m#{line_numbered_lua}\027[0m\n\n#{err}", 0)
            source_key = tostring(source)
            unless environment.SOURCE_MAP[source_key]
                map = {}
                file = Files.read(source.filename)
                if not file
                    error "Failed to find file: #{source.filename}"
                nomsu_str = file\sub(source.start, source.stop)
                lua_line = 1
                nomsu_line = Files.get_line_number(file, source.start)
                map_sources = (s)->
                    if type(s) == 'string'
                        for nl in s\gmatch("\n")
                            map[lua_line] or= nomsu_line
                            lua_line += 1
                    else
                        if s.source and s.source.filename == source.filename
                            nomsu_line = Files.get_line_number(file, s.source.start)
                        for b in *s.bits do map_sources(b)
                map_sources(to_run)
                map[lua_line] or= nomsu_line
                map[0] = 0
                -- Mapping from lua line number to nomsu line numbers
                environment.SOURCE_MAP[source_key] = map
            return run_lua_fn!
        else
            error("Attempt to run unknown thing: "..tostring(to_run))
    
    FILE_CACHE: {}
    run_file_1_in: (path, environment, optimization, prefix=nil)->
        if not optimization
            optimization = environment.OPTIMIZATION
        if environment.FILE_CACHE[path]
            import_to_1_from(environment, environment.FILE_CACHE[path], prefix)
            return
        if _currently_running_files\has(path)
            i = _currently_running_files\index_of(path)
            _currently_running_files\add path
            circle = _currently_running_files\from_1_to(i, -1)
            error("Circular import detected:\n           "..circle\joined_with("\n..imports  "))
        _currently_running_files\add path
        mod = _1_forked(environment)

        did_anything = false
        for nomsupath in package.nomsupath\gmatch("[^;]+")
            full_path = nomsupath == "." and path or nomsupath.."/"..path
            files = Files.list(full_path)
            continue unless files
            for filename in *files
                lua_filename = filename\gsub("%.nom$", ".lua")
                -- Need to check here to prevent re-running files
                if environment.FILE_CACHE[filename]
                    import_to_1_from(environment, environment.FILE_CACHE[filename], prefix)
                    continue
                -- TODO: don't automatically use precompiled version?
                code = if optimization != 0 and Files.read(lua_filename)
                    -- TODO: use a checksum?
                    file = Files.read(lua_filename)
                    LuaCode\from(Source(filename, 1, #file), file)
                else
                    file = Files.read(filename)
                    NomsuCode\from(Source(filename, 1, #file), file)
                environment.run_1_in(code, mod)
                did_anything = true
            break
        unless did_anything
            error("File not found: #{path}", 0)
        import_to_1_from(environment, mod, prefix)
        environment.FILE_CACHE[path] = mod
        _currently_running_files\remove!
}
nomsu_environment._ENV = nomsu_environment

-- Hacky use of globals:
export SOURCE_MAP
SOURCE_MAP = nomsu_environment.SOURCE_MAP

return nomsu_environment