-- Autogenerated with DRAKON Editor 1.33
local table = table
local string = string
local pairs = pairs
local ipairs = ipairs
local type = type
local tostring = tostring
local tonumber = tonumber
local print = print
local math = math
local os = os

local global_cfg = global_cfg
local globs = globs

local vud = require("vud")
local ej = require("ej")

local clock = require("clock")
local log = require("log")
local digest = require("digest")
local fiber = require("fiber")
local utils = require("utils")
local fun = require("fun")
local lic = require("lic")
local json = require("json")
local pretty = require("pretty.json")
local fio = require("fio")

local utf8 = require("lua-utf8")


local search_yield = 0.005
local current_version = 20180619
local root_folder_id = "1"

local db = require(global_cfg.db)

local default_html = "<!DOCTYPE html>\n" ..
"<html lang=\"en\">\n" ..
"<head>\n" ..
"  <meta charset=\"utf-8\"/>\n" ..
"  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n" ..
"  <link rel=\"shortcut icon\" href=\"https://app.drakon.tech/static/favicon.ico\" />\n" ..
"  <link rel=\"icon\" type=\"image/png\" href=\"https://app.drakon.tech/static/favicon.png\" />\n" ..
"  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css\" integrity=\"sha512-NmLkDIU1C/C88wi324HBc+S2kLhi08PN5GDeUVVVC/BVt/9Izdsc9SVeVfA1UZbY3sHUlDSyRXhCzHfr6hmPPw==\" crossorigin=\"anonymous\" />\n" ..
"  <style>\n" ..
"  *, *:before, *:after {\n" ..
"    -webkit-box-sizing: border-box;\n" ..
"    -moz-box-sizing: border-box;\n" ..
"    box-sizing: border-box;\n" ..
"  }\n" ..
"  </style>\n" ..
"  <title>@name@</title>\n" ..
"</head>\n" ..
"<body>\n" ..
"  <div id=\"main\"></div>\n" ..
"  <script src=\"@name@.js\"></script>\n" ..
"  <script>\n" ..
"  </script>\n" ..
"</body>\n" ..
"</html>"

local default_html_es6 = "<!DOCTYPE html>\n" ..
"<html lang=\"en\">\n" ..
"<head>\n" ..
"  <meta charset=\"utf-8\"/>\n" ..
"  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n" ..
"  <title>@name@</title>\n" ..

"</head>\n" ..
"<body>\n" ..
"  <h1>@name@</h1>\n" ..
"  <div id=\"main\"></div>\n" ..
"  <script type=\"module\">\n" ..
"   import * as @name@ from \"/gen/@gentoken@/@name@.js\";\n" ..
"  </script>\n" ..
"</body>\n" ..
"</html>"

setfenv(1, {}) 

function access(data, grant, user_id, roles)
    local message = access_core(
    	data,
    	grant,
    	user_id,
    	roles
    )
    if message then
        return message
    else
        return nil
    end
end

function access_core(data, grant, user_id, roles)
    local count = 0
    local space_id = data.workspace
    local message = check_admin_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return message, count
    else
        db.begin()
        if grant then
            for _, uid in ipairs(data.users) do
                log_user_event(
                	user_id,
                	"grant",
                	{principal=uid, space=space_id, 
                	op=data.operation}
                )
                db.rights_insert(
                	space_id,
                	uid,
                	data.operation,
                	{}
                )
            end
        else
            for _, uid in ipairs(data.users) do
                log_user_event(
                	user_id,
                	"revoke",
                	{principal=uid, space=space_id, 
                	op=data.operation}
                )
                db.rights_delete(
                	space_id,
                	uid,
                	data.operation
                )
            end
        end
        local sdata = db.space_get(space_id)
        local rights = db.rights_get_by_space(
        	space_id
        )
        local admins = get_admins(rights)
        if #admins == 0 then
            message = "ERR_LAST_ADMIN"
            db.rollback()
            return message, count
        else
            if ((grant) and (not (roles.admin))) and (not (data.operation == "read")) then
                local max_users = get_max_users(sdata)
                local actual_users = get_actual_users(
                	rights
                )
                if actual_users > max_users then
                    message = "ERR_USER_LIMIT"
                    count = actual_users
                    db.rollback()
                    return message, count
                else
                    update_space_limits_core(
                    	space_id,
                    	admins
                    )
                    db.commit()
                    return nil
                end
            else
                update_space_limits_core(
                	space_id,
                	admins
                )
                db.commit()
                return nil
            end
        end
    end
end

function add_child(space_id, folder_id, child_id, user_id)
    local fdata = db.folder_get(space_id, folder_id)
    if is_folder(fdata) then
        db.folder_tree_upsert(
        	space_id,
        	child_id,
        	folder_id
        )
        return nil
    else
        return "ERR_NOT_FOLDER"
    end
end

function add_default_html(space_id, folder_id, user_id, roles, props)
    if is_js(props) then
        if props.html then
            content = utils.trim(props.html)
            if #content == 0 then
                local ok, gt = get_create_gentoken(
                	space_id,
                	user_id,
                	roles
                )
                if ok then
                    local fdata = db.folder_get(
                    	space_id,
                    	folder_id
                    )
                    props.html = make_default_html(
                    	fdata.name,
                    	props.mformat,
                    	gt.gentoken
                    )
                end
            end
        else
            local ok, gt = get_create_gentoken(
            	space_id,
            	user_id,
            	roles
            )
            if ok then
                local fdata = db.folder_get(
                	space_id,
                	folder_id
                )
                props.html = make_default_html(
                	fdata.name,
                	props.mformat,
                	gt.gentoken
                )
            end
        end
    end
end

function add_to_recent(space_id, folder_id, user_id)
    local data = {
    	when = clock.time()
    }
    db.recent_upsert(
    	space_id,
    	folder_id,
    	user_id,
    	data
    )
    local urecent = db.recent_get_by_user(user_id)
    if #urecent > global_cfg.max_recent then
        local compare_when = function(left, right)
        	local lwhen = left[4].when
        	local rwhen = right[4].when
        	return lwhen < rwhen
        end
        table.sort(urecent, compare_when)
        local n = #urecent - global_cfg.max_recent
        local redundant = fun.take_n(n, urecent)
        for i, red in fun.iter(redundant) do
            db.recent_delete(
            	red[1],
            	red[2],
            	red[3]
            )
        end
    end
end

function add_to_trash(space_id, folders)
    for _, folder_id in ipairs(folders) do
        db.trash_insert(space_id, folder_id)
    end
end

function add_tree_node(space_id, folder_id, fdata, depth, lines)
    if is_folder(fdata) then
        local me = {
        	space_id = space_id,
        	id = folder_id,
        	name = fdata.name,
        	depth = depth
        }
        table.insert(lines, me)
        local children = {}
        local kids = get_child_folders(
        	space_id,
        	folder_id
        )
        for _, child_id in ipairs(kids) do
            local child_data = db.folder_get(
            	space_id,
            	child_id
            )
            table.insert(
            	children,
            	{
            		id = child_id,
            		fdata = child_data
            	}
            )
        end
        table.sort(
        	children,
        	compare_fdata
        )
        local cdepth = depth + 1
        for _, child in ipairs(children) do
            add_tree_node(
            	space_id,
            	child.id,
            	child.fdata,
            	cdepth,
            	lines
            )
        end
    end
end

function backup(space_id, user_id, roles)
    local ok, data = prepare_backup(
    	space_id,
    	user_id,
    	roles
    )
    if ok then
        local names = data
        backup_project(space_id, names.tmp)
        local command = "zip -r -j " .. names.path ..
        	" " .. names.tmp
        log.info(command)
        local cmd_result = os.execute(command)
        log.info(cmd_result)
        local time = os.date("%Y%m%d_%H%M", os.time())
        local down_name = space_id .. time .. ".zip"
        result = {
        	filename = down_name,
        	url = names.url
        }
        return true, result
    else
        return false, data
    end
end

function backup_folder(row, tmp)
    local space_id = row[1]
    local folder_id = row[2]
    local folder = row[3]
    local parent_id = db.folder_tree_get(
    	space_id,
    	folder_id
    )
    if folder.deleted then
        
    else
        folder.items = {}
        folder.props = {}
        folder.parent_id = parent_id
        local items = db.item_get_by_folder(
        	space_id,
        	folder_id
        )
        for _, item_row in ipairs(items) do
            local item_id = item_row[3]
            local item = item_row[4]
            folder.items[item_id] = item
        end
        local props = db.folder_props_get_by_folder(
        	space_id,
        	folder_id
        )
        for _, prop in ipairs(props) do
            local prop_name = prop[3]
            local prop_value = prop[4]
            folder.props[prop_name] = prop_value
        end
        local content = json.encode(folder)
        local path = tmp .. "/" .. folder_id .. ".json"
        utils.write_all_bytes(path, content)
    end
end

function backup_project(space_id, tmp)
    local sdata2 = {}
    local content = pretty.stringify(sdata2, nil, 4)
    local path = tmp .. "/project.json"
    utils.write_all_bytes(path, content)
    local folders = db.folder_get_by_space(space_id)
    for _, folder in ipairs(folders) do
        backup_folder(folder, tmp)
    end
end

function build_copy_plan(space_id, folder_id, plan)
    fiber.yield()
    local children = get_child_folders(
    	space_id,
    	folder_id
    )
    plan[folder_id] = utils.list_to_set(
    	children
    )
    for _, child_id in ipairs(children) do
        build_copy_plan(
        	space_id,
        	child_id,
        	plan
        )
    end
end

function by_count(left, right)
    return left.count > right.count
end

function calculate_depth(space_id, folder_id)
    local depth = 0
    while true do
        depth = depth + 1
        folder_id = db.folder_tree_get(
        	space_id,
        	folder_id
        )
        if folder_id then
            
        else
            break
        end
    end
    return depth
end

function can_edit_folder(space_id, folder_id, user_id, roles)
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        local fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        if fdata then
            if fdata.deleted then
                message = "ERR_NOT_FOUND"
                return false, message
            else
                return true, fdata
            end
        else
            message = "ERR_NOT_FOUND"
            return false, message
        end
    end
end

function check_admin_access(space_id, user_id, roles)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	roles
        )
        if access == "admin" then
            return nil
        else
            return "ERR_ACCESS_DENIED"
        end
    else
        return "ERR_NOT_FOUND"
    end
end

function check_dst_folder(space_id, folder_id, user_id, roles)
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return message
    else
        local fdata = find_folder(
        	space_id,
        	folder_id
        )
        if fdata then
            if is_folder(fdata) then
                return nil
            else
                return "ERR_NOT_FOLDER"
            end
        else
            return "ERR_NOT_FOUND"
        end
    end
end

function check_read_access(space_id, user_id, roles)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	roles
        )
        if ((access == "admin") or (access == "write")) or (access == "read") then
            return nil, access, sdata.public
        else
            return "ERR_ACCESS_DENIED"
        end
    else
        return "ERR_NOT_FOUND"
    end
end

function check_tree(diagrams, start_id, good)
    local visited = {}
    local id = start_id
    while true do
        if good[id] then
            return nil
        end
        if visited[id] then
            return "Cycle detected"
        end
        visited[id] = true
        local diagram = diagrams[id]
        if utils.is_empty(diagram.parent_id) then
            if id == "1" then
                id = start_id
                while true do
                    good[id] = true
                    local diagram = diagrams[id]
                    id = diagram.parent_id
                    if utils.is_empty(id) then
                        break
                    end
                end
                return nil
            else
                return "Wrong root"
            end
            break
        end
        id = diagram.parent_id
    end
end

function check_write_access(space_id, user_id, roles)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	roles
        )
        if (access == "admin") or (access == "write") then
            return nil
        else
            return "ERR_ACCESS_DENIED"
        end
    else
        return "ERR_NOT_FOUND"
    end
end

function clear_trash(space_id, user_id, roles)
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return message
    else
        db.begin()
        local folders = get_trash_folders(
        	space_id
        )
        for _, folder_id in ipairs(folders) do
            delete_forever(
            	space_id,
            	folder_id
            )
            remove_from_trash(
            	space_id,
            	folder_id
            )
        end
        db.commit()
        return nil
    end
end

function compare_fdata(left, right)
    local result = utf8.ncasecmp(
    	left.fdata.name,
    	right.fdata.name
    )
    return result < 0
end

function copyNotNil(src, name, dst)
    local value = src[name]
    if value == nil then
        
    else
        dst[name] = value
    end
end

function copy_folder(space_id, folder_id, dst_space_id, dst_folder_id, user_id, plan)
    fiber.yield()
    db.begin()
    local new_id
    local ok = true
    local message = nil
    local fdata = db.folder_get(
    	space_id,
    	folder_id
    )
    ok, new_id = create_folder_kernel(
    	dst_space_id,
    	fdata,
    	user_id,
    	fdata.version or 0,
    	dst_folder_id
    )
    if ok then
        local my_items = db.item_get_by_folder(
        	space_id,
        	folder_id
        )
        for _, item in ipairs(my_items) do
            local item_id = item[3]
            local idata = item[4]
            db.item_insert(
            	dst_space_id,
            	new_id,
            	item_id,
            	idata
            )
        end
        local props = db.folder_props_get_by_folder(
        	space_id,
        	folder_id
        )
        for _, prop in ipairs(props) do
            local prop_name = prop[3]
            local prop_value = prop[4]
            db.folder_props_insert(
            	dst_space_id,
            	new_id,
            	prop_name,
            	prop_value
            )
        end
        db.commit()
        local children = plan[folder_id]
        if children then
            for child_id, _ in pairs(children) do
                ok, message = copy_folder(
                	space_id,
                	child_id,
                	dst_space_id,
                	new_id,
                	user_id,
                	plan
                )
                if ok then
                    
                else
                    break
                end
            end
        end
    else
        db.rollback()
        message = new_id
    end
    return ok, message
end

function copy_many(items, dst_space_id, dst_folder_id, user_id, roles)
    local ok, message
    message = check_dst_folder(
    	dst_space_id,
    	dst_folder_id,
    	user_id,
    	roles
    )
    if message then
        return message
    else
        for _, item in ipairs(items) do
            local sdata = find_folder(
            	item.space_id,
            	item.id
            )
            if sdata then
                
            else
                message = "ERR_NOT_FOUND"
                return message
            end
            message = check_read_access(
            	item.space_id,
            	user_id,
            	roles
            )
            if message then
                return message
            end
        end
        for _, item in ipairs(items) do
            local plan = {}
            build_copy_plan(
            	item.space_id,
            	item.id,
            	plan
            )
            ok, message = copy_folder(
            	item.space_id,
            	item.id,
            	dst_space_id,
            	dst_folder_id,
            	user_id,
            	plan
            )
            if ok then
                
            else
                return message
            end
        end
        return nil
    end
end

function create_folder(space_id, fields, user_id, roles)
    local count = 0
    db.begin()
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        db.rollback()
        return false, message, count
    else
        local parent_id = extract_parent_id(
        	fields
        )
        local ok, result, dcount = create_folder_kernel(
        	space_id,
        	fields,
        	user_id,
        	current_version,
        	parent_id
        )
        if ok then
            db.commit()
            return true, result
        else
            message = result
            count = dcount
            db.rollback()
            return false, message, count
        end
    end
end

function create_folder_kernel(space_id, fields, user_id, version, parent_id)
    local sdata = db.space_get(space_id)
    local diagrams = get_diagram_count(space_id)
    local limit = get_max_diagrams(sdata)
    if (diagrams >= limit) and (not (is_folder(fields))) then
        return false, "ERR_DIAGRAM_LIMIT", diagrams + 1
    else
        local folder_id = generate_folder_id(space_id)
        db.space_update(
        	space_id,
        	sdata
        )
        local message = db_create_folder(
        	space_id,
        	folder_id,
        	fields,
        	user_id,
        	version,
        	parent_id
        )
        if message then
            return false, message
        else
            return true, folder_id
        end
    end
end

function create_gen_folder(gentoken)
    local path = global_cfg.gen_dir ..
      "/" .. gentoken
    return fio.mkdir(path)
end

function create_space(space_id, owner, root_id)
    local now = clock.time()
    local sdata = {
    	public = false,
    	when_created = now,
    	when_updated = now
    }
    db.space_insert(space_id, sdata)
    db.rights_insert(
    	space_id,
    	owner,
    	"admin",
    	{}
    )
    local fields = {
    	type = "folder",
    	name = "<root>"
    }
    db_create_folder(
    	space_id,
    	root_folder_id,
    	fields,
    	"system",
    	current_version,
    	nil
    )
    log_user_event(
    	owner,
    	"create_space",
    	{space = space_id}
    )
end

function db_create_folder(space_id, folder_id, fields, user_id, version, parent_id)
    local now = clock.time()
    local fdata = {}
    for key, value in pairs(fields) do
        fdata[key] = value
    end
    fdata.when_created = now
    fdata.when_updated = now
    fdata.created_by = user_id
    fdata.updated_by = user_id
    fdata.version = version
    fdata.tag = ""
    db.folder_insert(space_id, folder_id, fdata)
    if parent_id then
        return add_child(
        	space_id,
        	parent_id,
        	folder_id,
        	user_id
        )
    else
        return nil
    end
end

function def_query(data, user_id, roles)
    local match
    local found = {}
    local space_error, access, is_public = check_read_access(
    	data.space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        if data.tokens then
            local tokens = utils.map(
            	data.tokens,
            	utils.normalize_string
            )
            match = function(sid, fid, folder)
            	name_matches(
            		sid, fid, folder, 
            		tokens, found
            	)
            end
        else
            local lines = utils.map(
            	data.lines,
            	utils.normalize_string
            )
            match = function(sid, fid, folder)
            	folder_matches_reversed(
            		sid, fid, folder, 
            		lines, found
            	)
            end
        end
        for_space_folders(data.space_id, match)
        return true, {
        	items = found
        }
    end
end

function delete_folder_core(space_id, folder_id)
    db.folder_tree_delete(
    	space_id,
    	folder_id
    )
    db.folder_props_delete(
    	space_id,
    	folder_id
    )
    delete_items(space_id, folder_id)
    db.folder_delete(
    	space_id,
    	folder_id
    )
end

function delete_forever(space_id, folder_id)
    local children = get_child_folders(
    	space_id,
    	folder_id
    )
    for _, child_id in ipairs(children) do
        delete_forever(space_id, child_id)
    end
    delete_folder_core(
    	space_id,
    	folder_id
    )
end

function delete_items(space_id, folder_id)
    local my_items = db.item_get_by_folder(
    	space_id,
    	folder_id
    )
    for _, item in ipairs(my_items) do
        local item_id = item[3]
        db.item_delete(
        	space_id,
        	folder_id,
        	item_id
        )
    end
end

function delete_many(items, user_id, roles)
    local message
    local deleted = {}
    db.begin()
    for _, item in ipairs(items) do
        message = delete_one(
        	item.space_id,
        	item.id,
        	user_id,
        	roles,
        	deleted,
        	true
        )
        if message then
            log.error(message)
            db.rollback()
            return message
        end
    end
    db.commit()
    return nil
end

function delete_one(space_id, folder_id, user_id, roles, deleted, tell_parent)
    local message = nil
    local key = space_id .. "/" .. folder_id
    if deleted[key] then
        
    else
        deleted[key] = {
        	space_id = space_id,
        	folder_id = folder_id
        }
        local ok, result = can_edit_folder(
        	space_id,
        	folder_id,
        	user_id,
        	roles
        )
        if ok then
            local fdata = result
            if tell_parent then
                db.folder_tree_delete(
                	space_id,
                	folder_id
                )
                add_to_trash(
                	space_id,
                	{ folder_id }
                )
            end
            local tag = make_folder_tag()
            fdata.tag = tag
            remember_update(fdata, user_id)
            fdata.deleted = true
            db.folder_update(
            	space_id,
            	folder_id,
            	fdata
            )
            remove_from_recent(
            	space_id,
            	folder_id
            )
            local children = get_child_folders(
            	space_id,
            	folder_id
            )
            for _, child_id in ipairs(children) do
                message = delete_one(
                	space_id,
                	child_id,
                	user_id,
                	roles,
                	deleted,
                	false
                )
                if message then
                    log.info("delete one: " .. 
                    	message .. " " ..
                    	child_id  )
                    break
                end
            end
        else
            log.info("can edit: " .. result)
            message = result
        end
    end
    return message
end

function delete_recent(user_id)
    local found = db.recent_get_by_user(user_id)
    for _, item in ipairs(found) do
        local space_id = item[1]
        local folder_id = item[2]
        db.recent_delete(space_id, folder_id, user_id)
    end
    log_user_event(user_id, "delete_recent", {})
end

function delete_recent_and_folders(space_id)
    local recent = db.recent_by_space(space_id)
    for _, row in ipairs(recent) do
        db.recent_delete(
        	row[1],
        	row[2],
        	row[3]
        )
    end
    local trash = db.trash_get_by_space(space_id)
    for _, titem in ipairs(trash) do
        local folder_id = titem[2]
        remove_from_trash(
        	space_id,
        	folder_id
        )
    end
    local folders = db.folder_get_by_space(
    	space_id
    )
    for _, folder in ipairs(folders) do
        local folder_id = folder[2]
        delete_folder_core(
        	space_id,
        	folder_id
        )
    end
    local tree = db.folder_tree_get_by_space(
    	space_id
    )
    for _, item in ipairs(tree) do
        local folder_id = item[2]
        db.folder_tree_delete(
        	space_id,
        	folder_id
        )
    end
end

function delete_space(space_id, user_id, roles)
    local space = {}
    local message = check_admin_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        
    else
        space = db.space_get(space_id)
        db.rights_delete_by_space(space_id)
        delete_recent_and_folders(
        	space_id
        )
        db.space_delete(space_id)
        log_user_event(
        	user_id,
        	"vud_delete_space",
        	{space_id=space_id}
        )
    end
    return message, space.root_id
end

function delete_theme(user_id)
    if user_id then
        db.user_props_delete(user_id)
    end
end

function downloadapp(space_id, folder_id, user_id, roles)
    local result = nil
    local module = db.folder_get(
    	space_id,
    	folder_id
    )
    local names, message = prepare_app_download(
    	space_id,
    	module.name,
    	user_id,
    	roles
    )
    if message then
        
    else
        log.info(names.tmp)
        local mods, message = get_modules_for_app(
        	space_id,
        	folder_id,
        	user_id,
        	roles
        )
        if message then
            
        else
            local html = get_text_from_item(
            	space_id,
            	folder_id,
            	"html"
            )
            if html then
                local jsname = names.tmp .. "/" .. 
                	module.name .. ".js"
                write_index_js(mods, jsname)
                for _, mod in ipairs(mods) do
                    local src = global_cfg.gen_dir .. "/" ..
                    	mod.gentoken .. "/" ..
                    	mod.name .. ".js"
                    local command = "cp " .. src .. " " .. 
                    	names.tmp .. "/"
                    local cmd_result = os.execute(command)
                    if cmd_result == 0 then
                        
                    else
                        message = "Module not built: " .. mod.name
                        return nil, message
                    end
                end
                local html_name = names.tmp .. "/index.html"
                generate_html_without_paths(
                	html,
                	module.name,
                	mods,
                	html_name
                )
                local command = "zip -r -j " .. names.path ..
                	" " .. names.tmp
                log.info(command)
                local cmd_result = os.execute(command)
                log.info(cmd_result)
                result = {
                	filename = names.filename,
                	url = names.url
                }
            else
                message = "ERR_HTML_NOT_SPECIFIED"
            end
        end
    end
    return result, message
end

function edit(space_id, folder_id, data, user_id, roles)
    db.begin()
    local message = nil
    local ok, result = can_edit_folder(
    	space_id,
    	folder_id,
    	user_id,
    	roles
    )
    if ok then
        local fdata = result
        if fdata.tag == data.oldTag then
            copyNotNil(data, "name", fdata)
            copyNotNil(data, "style", fdata)
            copyNotNil(data, "keywords", fdata)
            copyNotNil(data, "params", fdata)
            copyNotNil(data, "tag", fdata)
            copyNotNil(data, "background", fdata)
            copyNotNil(data, "diaLine", fdata)
            copyNotNil(data, "diaLineThickness", fdata)
            copyNotNil(data, "font", fdata)
            remember_update(fdata, user_id)
            db.folder_update(
            	space_id,
            	folder_id,
            	fdata
            )
            if data.editType == "replace" then
                delete_items(space_id, folder_id)
                for _, ritem in ipairs(data.items) do
                    db.item_insert(
                    	space_id,
                    	folder_id,
                    	ritem.id,
                    	ritem
                    )
                end
            else
                local deleted = {}
                if data.removed then
                    for _, ritem in ipairs(data.removed) do
                        db.item_delete(
                        	space_id,
                        	folder_id,
                        	ritem
                        )
                        deleted[ritem] = true
                    end
                end
                if data.added then
                    for _, aitem in ipairs(data.added) do
                        db.item_insert(
                        	space_id,
                        	folder_id,
                        	aitem.id,
                        	aitem
                        )
                    end
                end
                if data.updated then
                    for _, uitem in ipairs(data.updated) do
                        update_item(
                        	space_id,
                        	folder_id,
                        	uitem.id,
                        	uitem,
                        	deleted
                        )
                    end
                end
            end
            db.commit()
            return nil
        else
            message = "ERR_MODIFIED"
            db.rollback()
            return message
        end
    else
        message = result
        db.rollback()
        return message
    end
end

function export_folder(space_id, folder_id, folder_list, item_list)
    local fdata = find_folder(space_id, folder_id)
    if fdata then
        local folder_item = {
        	folder_id = folder_id,
        	fdata = fdata
        }
        table.insert(folder_list, folder_item)
        local my_items = db.item_get_by_folder(
        	space_id,
        	folder_id
        )
        for _, item in ipairs(my_items) do
            local item_id = item[3]
            local fields = item[4]
            local item_record = {
            	folder_id = folder_id,
            	item_id = item_id,
            	fields = fields
            }
            table.insert(item_list, item_record)
        end
    end
end

function export_space(space_id, filename)
    local folder_list = {}
    local item_list = {}
    local folders = db.folder_get_by_space(
    	space_id
    )
    for _, folder in ipairs(folders) do
        local folder_id = folder[2]
        export_folder(
        	space_id,
        	folder_id,
        	folder_list,
        	item_list
        )
    end
    local result = {
    	folders = folder_list,
    	items = item_list
    }
    utils.write_json(filename, result)
end

function extend_life(self)
    local timeout = 20
    self.expiry = os.time() + timeout
end

function extract_multipart_body(payload)
    start = find_body_start(payload)
    body_end = find_body_end(payload)
    return payload:sub(start, body_end)
end

function extract_parent_id(fields)
    local parent_id = fields.parent
    fields.parent = nil
    if parent_id == "" then
        return nil
    else
        return parent_id
    end
end

function find_body_end(payload)
    i = #payload - 1
    while true do
        if i > 0 then
            
        else
            return -1
        end
        b1 = payload:byte(i)
        b2 = payload:byte(i + 1)
        if (b1 == 13) and (b2 == 10) then
            return i - 1
        end
        i = i - 1
    end
end

function find_body_start(payload)
    first, last = payload:find("\r\n\r\n")
    return last + 1
end

function find_cycle(space_id, moving_id, target_id)
    while true do
        if target_id then
            
        else
            return false
        end
        if target_id == moving_id then
            return true
        end
        target_id = db.folder_tree_get(
        	space_id,
        	target_id
        )
    end
end

function find_folder(space_id, folder_id)
    if folder_id then
        local fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        if (fdata) and (not (fdata.deleted)) then
            return fdata
        else
            return nil
        end
    else
        return nil
    end
end

function find_folder_by_name(space_id, name, user_id, roles)
    ej.info(
    	"find_folder_by_name",
    	{user_id=user_id, name=name,
    	space_id=space_id}
    )
    local space_error, a, p = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local needle = utf8.lower(name)
        local folders = {}
        local match = function(sid, fid, folder)
        	folder_has_name(
        		sid, fid, folder, 
        		needle, folders
        	)
        end
        for_space_folders(space_id, match)
        if #folders == 0 then
            return false, "ERR_NOT_FOUND"
        else
            return true, {id=folders[1]}
        end
    end
end

function find_folders(space_id, needle, user_id, roles)
    ej.info(
    	"find_folders",
    	{user_id=user_id, needle=needle,
    	space_id=space_id}
    )
    local space_error, a, p = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        needle = utils.normalize_string(needle)
        local folders = {}
        local match = function(sid, fid, folder)
        	folder_matches(
        		sid, fid, folder, 
        		needle, folders
        	)
        end
        for_space_folders(space_id, match)
        local result = {
        	folders = folders
        }
        return true, result
    end
end

function find_move_cycle(dst_space_id, dst_folder_id, space_id, folder_id)
    if (dst_space_id == space_id) and (find_cycle(dst_space_id, folder_id, dst_folder_id)) then
        return true
    else
        return false
    end
end

function find_startup(modules)
    for _, mod in ipairs(modules) do
        if mod.startup then
            return mod.name
        end
    end
    return nil
end

function folder_has_name(space_id, folder_id, folder, needle, result)
    local fname = utf8.lower(folder.name)
    if fname == needle then
        table.insert(result, folder_id)
    end
end

function folder_matches(space_id, folder_id, folder, needle, result)
    if norm_contains(folder.name, needle) then
        local folder_info = make_found_folder(
        	space_id,
        	folder_id,
        	folder
        )
        table.insert(result, folder_info)
    end
end

function folder_matches_reversed(space_id, folder_id, folder, lines, result)
    local name = utils.normalize_string(
    	folder.name
    )
    for _, line in ipairs(lines) do
        if utils.string_contains_words(line, name) then
            local folder_info = make_found_folder(
            	space_id,
            	folder_id,
            	folder
            )
            table.insert(result, folder_info)
            break
        end
    end
end

function for_space_folders(space_id, action)
    local folders = db.folder_get_by_space(
    	space_id
    )
    for _, row in ipairs(folders) do
        local folder_id = row[2]
        local folder = row[3]
        if ((folder) and (not (folder_id == "1"))) and (not (is_deleted(folder))) then
            action(
            	space_id,
            	folder_id,
            	folder
            )
        end
    end
end

function genapp(space_id, folder_id, user_id, roles)
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        
    else
        local ok, gen_result = get_create_gentoken(
        	space_id,
        	user_id,
        	roles
        )
        if ok then
            local module = db.folder_get(
            	space_id,
            	folder_id
            )
            local mods, message = get_modules_for_app(
            	space_id,
            	folder_id,
            	user_id,
            	roles
            )
            if message then
                
            else
                local html = get_text_from_item(
                	space_id,
                	folder_id,
                	"html"
                )
                if html then
                    local jsfolder = global_cfg.gen_dir .. "/" .. 
                      gen_result.gentoken ..
                      "/" .. module.name
                    fio.mkdir(jsfolder)
                    local jsname = jsfolder .. "/" .. 
                    	module.name .. ".js"
                    write_index_js(mods, jsname)
                    local html_name = jsfolder .. "/index.html"
                    generate_html_with_paths(
                    	html,
                    	module.name,
                    	mods,
                    	html_name
                    )
                else
                    message = "ERR_HTML_NOT_SPECIFIED"
                end
            end
        else
            message = gen_result
        end
    end
    return message
end

function generate_folder_id(space_id)
    local id
    while true do
        local rnd = utils.random_string()
        id = rnd:sub(1, 8)
        if db.folder_get(space_id, id) then
            
        else
            break
        end
    end
    return id
end

function generate_html_with_paths(html, module_name, mods, filename)
    local scripts = "\n"
    for _, mod in ipairs(mods) do
        local surl = "/gen/" .. mod.gentoken ..
        	"/" .. mod.name .. ".js"
        scripts = scripts .. "    <script src=\"" ..
        	surl ..	"\"></script>\n"
    end
    scripts = scripts .. "    <script src=\"" .. module_name ..
    	".js\"></script>\n"
    local final_html = replace_string(
    	html,
    	"%SCRIPT%",
    	scripts
    )
    utils.write_all_bytes(filename, final_html)
end

function generate_html_without_paths(html, module_name, mods, filename)
    local scripts = "\n"
    for _, mod in ipairs(mods) do
        local surl = mod.name .. ".js"
        scripts = scripts .. "    <script src=\"" ..
        	surl ..	"\"></script>\n"
    end
    scripts = scripts .. "    <script src=\"" .. module_name ..
    	".js\"></script>\n"
    local final_html = replace_string(
    	html,
    	"%SCRIPT%",
    	scripts
    )
    utils.write_all_bytes(filename, final_html)
end

function get_access(sdata, space_id, user_id, roles)
    if roles.admin then
        return "admin"
    else
        if user_id == "" then
            if (sdata.public) or (roles.system) then
                return "read"
            else
                return nil
            end
        else
            local accesses = get_accesses(
            	space_id,
            	user_id
            )
            if accesses.admin then
                return "admin"
            else
                if accesses.write then
                    return "write"
                else
                    if accesses.read then
                        return "read"
                    else
                        if (sdata.public) or (roles.system) then
                            return "read"
                        else
                            return nil
                        end
                    end
                end
            end
        end
    end
end

function get_access_for_space(space_id)
    local rights = db.rights_get_by_space(
    	space_id
    )
    local readers = {}
    local writers = {}
    local admins = {}
    for _, right in ipairs(rights) do
        local user_id = right[2]
        local access = right[3]
        if access == "admin" then
            admins[user_id] = true
        else
            if access == "write" then
                writers[user_id] = true
            else
                if access == "read" then
                    
                else
                    error(access)
                end
                readers[user_id] = true
            end
        end
    end
    return {
    	readers = utils.set_to_list(readers),
    	writers = utils.set_to_list(writers),
    	admins = utils.set_to_list(admins)
    }
end

function get_accesses(space_id, user_id)
    local rights = db.rights_get_by_space_user(
    	space_id,
    	user_id
    )
    local accesses = {}
    for _, right in ipairs(rights) do
        local access = right[3]
        accesses[access] = true
    end
    return accesses
end

function get_actual_users(rights)
    local all = {}
    for _, right in ipairs(rights) do
        local access = right[3]
        local user_id = right[2]
        if access == "read" then
            
        else
            all[user_id] = true
        end
    end
    local all_list = utils.set_to_list(all)
    return #all_list
end

function get_admins(rights)
    local admins = {}
    for _, right in ipairs(rights) do
        local access = right[3]
        local user_id = right[2]
        if access == "admin" then
            table.insert(
            	admins,
            	user_id
            )
        end
    end
    return admins
end

function get_basic_limits()
    local product = utils.get_product("basic")
    return {
    	max_spaces = product.max_spaces,
    	max_diagrams = product.max_diagrams,
    	max_users = product.max_users
    }
end

function get_child_folders(space_id, parent_id)
    local get_folder_id = function(row)
    	return row[2]
    end
    local rows = db.folder_tree_get_by_parent(
    	space_id,
    	parent_id
    )
    local result = utils.map(
    	rows,
    	get_folder_id
    )
    return result
end

function get_children(space_id, folder_id)
    local children = {}
    local language = nil
    local path_info
    local kids = get_child_folders(
    	space_id,
    	folder_id
    )
    for _, child_id in ipairs(kids) do
        local cdata = db.folder_get(
        	space_id,
        	child_id
        )
        if (cdata) and (not (is_deleted(cdata))) then
            local item = {
            	space_id = space_id,
            	id = child_id,
            	type = cdata.type,
            	name = cdata.name,
            	params = cdata.params,
            	keywords = cdata.keywords
            }
            if cdata.type == "module" then
                path_info = get_path(space_id, child_id)
                item.language = path_info.language
                item.module = path_info.module
                item.module_name = path_info.module_name
                item.mformat = path_info.mformat
            end
            table.insert(children, item)
        end
    end
    return children
end

function get_create_gentoken(space_id, user_id, roles)
    local gentoken = nil
    local result = nil
    local ok = nil
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        ok = false
        result = space_error
    else
        local sdata = db.space_get(space_id)
        if sdata.gentoken then
            gentoken = sdata.gentoken
            result = {
            	gentoken = gentoken
            }
            ok = true
        else
            gentoken = utils.random_password(32)
            if create_gen_folder(gentoken) then
                sdata.gentoken = gentoken
                db.space_update(
                	space_id,
                	sdata
                )
                result = {
                	gentoken = gentoken
                }
                ok = true
            else
                ok = false
                result = "ERROR_COULD_NOT_CREATE_FOLDER"
            end
        end
    end
    return ok, result
end

function get_diagram_count(space_id)
    local count = 0
    local folders = db.folder_get_by_space(
    	space_id
    )
    for _, folder in ipairs(folders) do
        local fdata = folder[3]
        if (is_folder(fdata)) or (fdata.deleted) then
            
        else
            count = count + 1
        end
    end
    return count
end

function get_drakons(space_id, folder_id, drakons)
    local _sw34870000_ = 0
    local children = get_children(
    	space_id,
    	folder_id
    )
    for _, child in ipairs(children) do
        _sw34870000_ = child.type
        if _sw34870000_ == "drakon" then
            table.insert(
            	drakons,
            	child
            )
        else
            if _sw34870000_ == "folder" then
                get_drakons(
                	child.space_id,
                	child.id,
                	drakons
                )
            end
        end
    end
end

function get_first_admin(space_id)
    local sdata = db.space_get(space_id)
    return sdata.admins[1]
end

function get_folder(space_id, folder_id, visit, user_id, roles)
    local path_info
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local fdata = find_folder(space_id, folder_id)
        if fdata then
            if ((user_id == "") or (not (visit))) or (is_folder(fdata)) then
                
            else
                add_to_recent(
                	space_id,
                	folder_id,
                	user_id
                )
            end
            path_info = get_path(space_id, folder_id)
            local items = get_items(space_id, folder_id)
            local children = get_children(space_id, folder_id)
            local parent = db.folder_tree_get(
            	space_id,
            	folder_id
            )
            if path_info.module then
                for _, child in ipairs(children) do
                    child.module = path_info.module
                    child.module_name = path_info.module_name
                    child.mformat = path_info.mformat
                end
            end
            local result = {
            	id = folder_id,
            	space_id = space_id,
            	name = fdata.name,
            	style = fdata.style,
            	type = fdata.type,
            	tag = fdata.tag,
            	params = fdata.params,
            	keywords = fdata.keywords,
            	parent = parent,
            	description = fdata.description,
            	access = access,
            	children = children,
            	items = items,
            	path = path_info.path,
            	is_public = is_public,
            	version = fdata.version or 0,
            	language = path_info.language,
            	module = path_info.module,
            	module_name = path_info.module_name,
            	mformat = path_info.mformat
            }
            return true, result
        else
            return false, "ERR_NOT_FOUND"
        end
    end
end

function get_folder_props(space_id, folder_id, user_id, roles)
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local result = get_folder_props_core(
        	space_id,
        	folder_id
        )
        add_default_html(
        	space_id,
        	folder_id,
        	user_id,
        	roles,
        	result
        )
        return true, result
    end
end

function get_folder_props_core(space_id, folder_id)
    local props = db.folder_props_get_by_folder(
    	space_id,
    	folder_id
    )
    local result = {}
    for _, prop in ipairs(props) do
        result[prop[3]] = prop[4]
    end
    return result
end

function get_item_secondary(item)
    return item.secondary or ""
end

function get_item_text(item)
    return item.text or ""
end

function get_item_text2(item)
    return item.text2 or ""
end

function get_items(space_id, folder_id)
    local result = {}
    local my_items = db.item_get_by_folder(
    	space_id,
    	folder_id
    )
    for _, item in ipairs(my_items) do
        local item_id = item[3]
        local idata = item[4]
        idata.id = item_id
        table.insert(result, idata)
    end
    return result
end

function get_language(space_id, folder_id)
    return db.folder_props_get(
    	space_id,
    	folder_id,
    	"language"
    )
end

function get_max_access(space_id, user_id)
    local accesses = get_accesses(
    	space_id,
    	user_id
    )
end

function get_max_diagrams(sdata)
    if sdata.max_diagrams then
        return sdata.max_diagrams
    else
        local limits = get_basic_limits()
        return limits.max_diagrams
    end
end

function get_max_users(sdata)
    if sdata.max_users then
        return sdata.max_users
    else
        local limits = get_basic_limits()
        return limits.max_users
    end
end

function get_module(space_id, folder_id, user_id, roles)
    local space_error, access, is_public =
     check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local fdata = find_folder(
        	space_id,
        	folder_id
        )
        if fdata then
            local drakons = {}
            get_drakons(
            	space_id,
            	folder_id,
            	drakons
            )
            local result = {
            	id = folder_id,
            	space_id = space_id,
            	name = fdata.name,
            	type = fdata.type,
            	tag = fdata.tag,
            	description = fdata.description,
            	diagrams = drakons
            }
            return true, result
        else
            return false, "ERR_NOT_FOUND"
        end
    end
end

function get_modules(space_id, language, user_id, roles)
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local folders = db.folder_get_by_space(
        	space_id
        )
        local all_modules = {}
        for _, folder in ipairs(folders) do
            local fdata = folder[3]
            if (fdata.type == "module") and (not (fdata.deleted)) then
                local module = {
                	space_id = space_id,
                	id = folder[2],
                	name = fdata.name
                }
                table.insert(all_modules, module)
            end
        end
        local modules = {}
        for _, mod in ipairs(all_modules) do
            local folder_id = mod.id
            mod.props = get_folder_props_core(
            	space_id,
            	folder_id
            )
            if mod.props.language == language then
                table.insert(modules, mod)
            end
        end
        return true, {modules=modules}
    end
end

function get_modules_for_app(space_id, folder_id, user_id, roles)
    local message = nil
    local mods_text = get_text_from_item(
    	space_id,
    	folder_id,
    	"modules"
    )
    if mods_text then
        local mods = json.decode(mods_text)
        message = get_tokens_for_modules(
        	mods,
        	user_id,
        	roles
        )
        if message then
            return false, message
        else
            return mods, nil
        end
    else
        message = "ERR_MODULES_NOT_SPECIFIED"
        return false, message
    end
end

function get_parent_id_for_restore(space_id, folder_id)
    local parent_id = db.folder_tree_get(
    	space_id,
    	folder_id
    )
    if parent_id then
        local parent = db.folder_get(
        	space_id,
        	parent_id
        )
        if parent.deleted then
            parent_id = root_folder_id
        end
    else
        parent_id = root_folder_id
    end
    return parent_id
end

function get_path(space_id, folder_id)
    local path = {}
    local language = nil
    local module = nil
    local module_name = nil
    local mformat = nil
    local fdata
    while true do
        fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        if fdata.type == "module" then
            language = db.folder_props_get(
            	space_id,
            	folder_id,
            	"language"
            )
            mformat = db.folder_props_get(
            	space_id,
            	folder_id,
            	"mformat"
            )
            module = folder_id
            module_name = fdata.name
        end
        table.insert(
        	path,
        	{
        		space_id = space_id,
        		id = folder_id,
        		name = fdata.name
        	}
        )
        folder_id = db.folder_tree_get(
        	space_id,
        	folder_id
        )
        if folder_id then
            
        else
            break
        end
    end
    local rpath = utils.reverse(path)
    local first = rpath[1]
    first.name = utf8.upper(space_id)
    return {
    	path = rpath,
    	language = language,
    	module = module,
    	module_name = module_name,
    	mformat = mformat
    }
end

function get_prog_modules(space_id, user_id, roles)
    local message = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        local result = {
        	modules = {}
        }
        local folders = db.folder_get_by_space(
        	space_id
        )
        for _, folder in ipairs(folders) do
            local folder_id = folder[2]
            local fdata = folder[3]
            if (fdata.deleted) or (not (fdata.type == "module")) then
                
            else
                local language = get_language(
                	space_id,
                	folder_id
                )
                if (language) and (not (language == "LANG_HUMAN")) then
                    local module = {
                    	space_id = space_id,
                    	folder_id = folder_id,
                    	name = fdata.name,
                    	language = language
                    }
                    table.insert(
                    	result.modules,
                    	module
                    )
                end
            end
        end
        return true, result
    end
end

function get_recent(user_id)
    local result = {}
    if user_id == "" then
        
    else
        local found = db.recent_get_by_user(user_id)
        for _, item in ipairs(found) do
            local record = {
                space_id = item[1],
                folder_id = item[2],
                whenOpened = item[4].when
            }
            local folder = db.folder_get(
            	record.space_id,
            	record.folder_id
            )
            local path_info = get_path(
            	record.space_id,
            	record.folder_id
            )
            record.name = folder.name
            record.type = folder.type
            record.path = path_info.path
            record.language = path_info.language
            record.module = path_info.module
            record.module_name = path_info.module_name
            record.mformat = path_info.mformat
            table.insert(result, record)
        end
        local compare_name = function(left, right)
        	local lname = left.name
        	local rname = right.name
        	local comp = utf8.ncasecmp(lname, rname)
        	return comp < 0
        end
        local compare_when = function(left, right)
        	return left.whenOpened > right.whenOpened
        end
        table.sort(
        	result,
        	compare_when
        )
    end
    return true, {
    	recent = result
    }
end

function get_search(session_id)
    return globs.searches[session_id]
end

function get_short_path(space_id, folder_id)
    local path = {}
    while true do
        local fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        table.insert(path, fdata.name)
        folder_id = db.folder_tree_get(
        	space_id,
        	folder_id
        )
        if folder_id == "1" then
            break
        end
    end
    table.insert(path, space_id)
    return utils.reverse(path)
end

function get_space_access(space_id, user_id, roles)
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return nil
    else
        return access
    end
end

function get_space_diagrams(space_id)
    local diagrams = {}
    local match = function(sid, fid, folder)
    	match_diagrams(
    		sid, fid, folder, 
    		diagrams
    	)
    end
    for_space_folders(space_id, match)
    return diagrams
end

function get_tag(space_id, folder_id)
    local fdata = find_folder(space_id, folder_id)
    if fdata then
        return true, fdata.tag
    else
        return false, "ERR_NOT_FOUND"
    end
end

function get_text_from_item(space_id, folder_id, item_id)
    local item = db.item_get(
    	space_id,
    	folder_id,
    	item_id
    )
    if (item) and (item.text) then
        local result = utils.trim(item.text)
        if result == "" then
            return nil
        else
            return result
        end
    else
        return nil
    end
end

function get_theme(user_id)
    local rows = db.user_props_get_by_user(user_id)
    local result = {}
    for _, row in ipairs(rows) do
        result[row[2]] = row[3]
    end
    return true, result
end

function get_tokens_for_modules(mods, user_id, roles)
    for _, mod in ipairs(mods) do
        ok, modgen = get_create_gentoken(
        	mod.spaceId,
        	user_id,
        	roles
        )
        if ok then
            
        else
            return modgen
        end
        local sdata = db.space_get(mod.spaceId)
        local access = get_access(
        	sdata,
        	mod.spaceId,
        	user_id,
        	roles
        )
        mod.gentoken = modgen.gentoken
        mod.access = access
    end
    return nil
end

function get_trash(space_id, user_id, roles)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local folders = get_trash_folders(
        	space_id
        )
        local result = {
        	access = access,
        	items = {}
        }
        for _, folder_id in ipairs(folders) do
            local fdata = db.folder_get(space_id, folder_id)
            local item = {
            	space_id = space_id,
            	id = folder_id,
            	type = fdata.type,
            	name = fdata.name
            }
            table.insert(
            	result.items,
            	item
            )
        end
        return true, result
    end
end

function get_trash_folders(space_id)
    local rows = db.trash_get_by_space(space_id)
    local get_folder_id = function(row)
    	return row[2]
    end
    return utils.map(
    	rows,
    	get_folder_id
    )
end

function get_tree(space_id, user_id, roles)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local lines = {}
        local root = db.folder_get(
        	space_id,
        	"1"
        )
        add_tree_node(
        	space_id,
        	"1",
        	root,
        	0,
        	lines
        )
        local result = {
        	lines = lines
        }
        return true, result
    end
end

function get_user_limits(user_id)
    local user = vud.get_user(user_id)
    if user then
        local license = lic.get_license(user.license)
        if license then
            return license.limits
        else
            return get_basic_limits()
        end
    else
        return get_basic_limits()
    end
end

function import_space(space_id, filename, owner)
    create_space(space_id, owner)
    db.folder_delete(
    	space_id,
    	"1"
    )
    for _, folder in ipairs(obj.folders) do
        db.folder_insert(
        	space_id,
        	folder.folder_id,
        	folder.fdata
        )
    end
    for _, item in ipairs(obj.items) do
        db.item_insert(
        	space_id,
        	item.folder_id,
        	item.item_id,
        	item.fields
        )
    end
end

function insert_items_from_map(space_id, folder_id, items_map)
    if items_map then
        for item_id, item in pairs(items_map) do
            item.id = nil
            db.item_insert(
            	space_id,
            	folder_id,
            	item_id,
            	item
            )
        end
    end
end

function is_deleted(folder)
    local deleted = not not folder.deleted
    return deleted
end

function is_diagram_like(fields, names)
    if is_folder(fields) then
        return false
    else
        local dia_name = utils.parse_compound_name(
        	fields.name
        )
        for _, name in ipairs(names) do
            if name == dia_name then
                return true
            end
        end
        return false
    end
end

function is_folder(obj)
    local _sw31850000_ = 0
    _sw31850000_ = obj.type
    if ((_sw31850000_ == "folder") or (_sw31850000_ == "module")) or (_sw31850000_ == "class") then
        return true
    else
        return false
    end
end

function is_js(props)
    if ((props.language == "LANG_JS") or (props.language == "LANG_JS2")) or (props.language == "LANG_S4") then
        return true
    else
        return false
    end
end

function load_project(folder)
    local ext = ".json"
    local files = fio.listdir(folder)
    local diagrams = {}
    for _, filename in ipairs(files) do
        if utils.ends_with(filename, ext) then
            local id = filename:sub(
            	1,
            	#filename - #ext
            )
            if id == "project" then
                
            else
                local path = folder .. "/" .. filename
                local content = utils.read_all_bytes(path)
                local obj = json.decode(content)
                diagrams[id] = obj
            end
        end
    end
    local good = {}
    local error = nil
    local root = diagrams["1"]
    if root then
        if utils.is_empty(root.parent_id) then
            for id, diagram in pairs(diagrams) do
                error = check_tree(
                	diagrams,
                	id,
                	good
                )
                if error then
                    log.error(error)
                    return false, ""
                end
            end
            return diagrams
        else
            log.error("1 is not root")
            return false, ""
        end
    else
        log.error("No root")
        return false, ""
    end
end

function log_user_event(user_id, type, data)
    data.user_id = user_id
    ej.info(type, data)
end

function make_default_html(name, mformat, gentoken)
    local template
    if mformat == "MES_ES6" then
        template = default_html_es6
    else
        template = default_html
    end
    local p2 = template:gsub("@name@", name)
    return p2:gsub("@gentoken@", gentoken)
end

function make_folder_tag()
    local t = clock.time()
    return tostring(t * 10000)
end

function make_found_folder(space_id, folder_id, folder)
    local result = {
    	space_id = space_id,
    	folder_id = folder_id,
    	name = folder.name,
    	type = folder.type,
    	path = get_short_path(space_id, folder_id)
    }
    return result
end

function match_diagrams(space_id, folder_id, folder, result)
    if is_folder(folder) then
        
    else
        local folder_info = {
        	space_id = space_id,
        	folder_id = folder_id,
        	fields = folder
        }
        table.insert(result, folder_info)
    end
end

function move_many(items, dst_space_id, dst_folder_id, user_id, roles)
    local message = check_dst_folder(
    	dst_space_id,
    	dst_folder_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        for _, item in ipairs(items) do
            if item.space_id == dst_space_id then
                
            else
                message = "ERR_CANT_MOVE_ACROSS"
                return false, message
            end
            local ok, fdata = can_edit_folder(
            	item.space_id,
            	item.id,
            	user_id,
            	roles
            )
            if ok then
                
            else
                message = fdata
                return false, message
            end
            local cycle = find_move_cycle(
            	dst_space_id,
            	dst_folder_id,
            	item.space_id,
            	item.id
            )
            if cycle then
                message = "ERR_CYCLE"
                return false, message
            end
        end
        local sorted = sort_for_move(items)
        for _, item in ipairs(sorted) do
            message = move_via_reparent(
            	item.space_id,
            	item.id,
            	dst_folder_id,
            	user_id
            )
            if message then
                return false, message
            end
        end
        local result = {
        	items = sorted
        }
        return true, result
    end
end

function move_via_reparent(space_id, folder_id, new_parent, user_id)
    db.folder_tree_upsert(
    	space_id,
    	folder_id,
    	new_parent
    )
    return nil
end

function multi_access(data, user_id, roles)
    for _, block in ipairs(data.blocks) do
        local grant = (block.action == "grant")
        local message, count = access_core(
        	block,
        	grant,
        	user_id,
        	roles
        )
        if message then
            return message, count
        end
    end
    if data.setPublicAccess then
        local sdata = db.space_get(data.spaceId)
        sdata.public = data.publicAccess
        db.space_update(
        	data.spaceId,
        	sdata
        )
        log_user_event(
        	user_id,
        	"public access",
        	{space=data.spaceId, public=sdata.public}
        )
    end
    return nil
end

function name_matches(space_id, folder_id, folder, tokens, result)
    local name = utils.normalize_string(
    	folder.name
    )
    for _, token in ipairs(tokens) do
        if token == name then
            local folder_info = make_found_folder(
            	space_id,
            	folder_id,
            	folder
            )
            table.insert(result, folder_info)
            break
        end
    end
end

function norm_contains(haystack, needle)
    if haystack then
        local low = utf8.lower(
        	haystack
        )
        local contains = utils.string_contains(
        	low,
        	needle
        )
        return contains
    else
        return false
    end
end

function prepare_app_download(space_id, module_name, user_id, roles)
    local message = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        local ok, gentoken_result = get_create_gentoken(
        	space_id,
        	user_id,
        	roles
        )
        if ok then
            local names = {}
            names.filename = module_name .. ".zip"
            names.url = "/gen/" .. gentoken_result.gentoken ..
            	"/" .. names.filename
            names.folder = global_cfg.gen_dir .. "/" .. 
            	gentoken_result.gentoken .. "/"
            names.path = names.folder .. names.filename
            names.tmp = os.tmpname()
            os.remove(names.tmp)
            os.remove(names.path)
            ok, message = fio.mktree(names.tmp)
            if ok then
                return names
            else
                log.error("Could not re-create tmp dir for backup/restore: " ..
                	message)
                return false, "ERR_SERVER"
            end
        else
            return false, gentoken_result
        end
    end
end

function prepare_backup(space_id, user_id, roles)
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        local ok, gentoken_result = get_create_gentoken(
        	space_id,
        	user_id,
        	roles
        )
        if ok then
            local names = {}
            names.filename = space_id .. ".zip"
            names.url = "/gen/" .. gentoken_result.gentoken ..
            	"/" .. names.filename
            names.folder = global_cfg.gen_dir .. "/" .. 
            	gentoken_result.gentoken .. "/"
            names.path = names.folder .. names.filename
            names.tmp = names.folder .. space_id
            fio.rmtree(names.tmp)
            os.remove(names.path)
            ok, message = fio.mktree(names.tmp)
            if ok then
                return ok, names
            else
                log.error("Could not re-create tmp dir for backup/restore: " ..
                	message)
                return false, "ERR_SERVER"
            end
        else
            return false, gentoken_result
        end
    end
end

function read_access(space_id, user_id, roles)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local sdata = db.space_get(space_id)
        local result = get_access_for_space(
        	space_id
        )
        result.public = sdata.public
        return true, result
    end
end

function remember_update(fields, user_id)
    fields.updated_by = user_id
    fields.when_updated = clock.time()
end

function remove_from_recent(space_id, folder_id)
    local found = db.recent_get_by_folder(
    	space_id,
    	folder_id
    )
    for _, record in ipairs(found) do
        local user_id = record[3]
        db.recent_delete(
        	space_id,
        	folder_id,
        	user_id
        )
    end
end

function remove_from_trash(space_id, folder_id)
    db.trash_delete(space_id, folder_id)
end

function remove_gen_folder(gentoken)
    local path = global_cfg.gen_dir ..
      "/" .. gentoken
    local pattern = path .. "/*"
    local files = fio.glob(pattern)
    for _, file in ipairs(files) do
        if os.remove(file) then
            
        else
            return false
        end
    end
    return os.remove(path)
end

function replace_string(text, needle, with)
    local first, last = string.find(
    	text,
    	needle,
    	1,
    	true
    )
    if first then
        local before = string.sub(text, 1, first - 1)
        local after = string.sub(text, last + 1, #text)
        return before .. with .. after
    else
        return text
    end
end

function reset_gentoken(space_id, user_id, roles)
    local result = nil
    local ok = nil
    local space_error = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        ok = false
        result = space_error
    else
        db.begin()
        local sdata = db.space_get(space_id)
        if sdata.gentoken then
            if remove_gen_folder(sdata.gentoken) then
                sdata.gentoken = nil
                db.space_update(
                	space_id,
                	sdata
                )
                db.commit()
                ok, result = get_create_gentoken(
                	space_id,
                	user_id,
                	roles
                )
            else
                db.rollback()
                ok = false
                result = "ERROR_COULD_NOT_REMOVE_FOLDER"
            end
        else
            db.commit()
            ok, result = get_create_gentoken(
            	space_id,
            	user_id,
            	roles
            )
        end
    end
    return ok, result
end

function resolve_deps(src, module, instances)
    local mod_var = instances[module.name]
    for _, dep in ipairs(module.deps) do
        local dep_var = instances[dep.module]
        src = src .. mod_var .. "." ..
        	dep.name .. " = " ..
        	dep_var .. ";\n"
    end
    return src
end

function restore(space_id, folder_id, user_id, roles)
    db.begin()
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        db.rollback()
        return message
    else
        local fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        if fdata then
            if fdata.deleted then
                local parent_id = get_parent_id_for_restore(
                	space_id,
                	folder_id
                )
                local tag = make_folder_tag()
                fdata.tag = tag
                fdata.deleted = false
                remember_update(fdata, user_id)
                db.folder_tree_upsert(
                	space_id,
                	folder_id,
                	parent_id
                )
                db.folder_update(
                	space_id,
                	folder_id,
                	fdata
                )
                local children = get_child_folders(
                	space_id,
                	folder_id
                )
                for _, child_id in ipairs(children) do
                    restore_recursive(
                    	space_id,
                    	child_id
                    )
                end
                add_child(
                	space_id,
                	parent_id,
                	folder_id,
                	user_id
                )
                remove_from_trash(
                	space_id,
                	folder_id
                )
                db.commit()
                return nil
            else
                message = "ERR_NOT_DELETED"
                db.rollback()
                return message
            end
        else
            message = "ERR_NOT_FOUND"
            db.rollback()
            return message
        end
    end
end

function restore_backup(space_id, body, user_id, roles)
    ok, data = prepare_backup(
    	space_id,
    	user_id,
    	roles
    )
    if ok then
        names = data
        content = extract_multipart_body(body)
        utils.write_all_bytes(names.path, content)
        command = "unzip " .. names.path ..
        	" -d " .. names.tmp
        log.info(command)
        cmd_result = os.execute(command)
        log.info(cmd_result)
        if cmd_result == 0 then
            local diagrams = load_project(
            	names.tmp
            )
            if diagrams then
                delete_recent_and_folders(
                	space_id
                )
                local parents = {}
                for folder_id, diagram in pairs(diagrams) do
                    if diagram.parent_id then
                        parents[folder_id] = diagram.parent_id
                    end
                    restore_diagram(
                    	space_id,
                    	folder_id,
                    	diagram
                    )
                end
                for folder_id, diagram in pairs(diagrams) do
                    local parent_id = parents[folder_id]
                    if utils.is_empty(parent_id) then
                        
                    else
                        add_child(
                        	space_id,
                        	parent_id,
                        	folder_id,
                        	diagram.created_by
                        )
                    end
                end
                return true, {}
            else
                return false, "Errors in structure"
            end
        else
            return false, "Could not unzip"
        end
    else
        return false, data
    end
end

function restore_diagram(space_id, folder_id, diagram)
    local items = diagram.items or {}
    local props = diagram.props or {}
    diagram.parent_id = nil
    diagram.items = nil
    diagram.props = nil
    db.folder_insert(space_id, folder_id, diagram)
    for name, value in pairs(props) do
        db.folder_props_insert(
        	space_id,
        	folder_id,
        	name,
        	value
        )
    end
    for item_id, item in pairs(items) do
        db.item_insert(
        	space_id,
        	folder_id,
        	item_id,
        	item
        )
    end
end

function restore_recursive(space_id, folder_id)
    local fdata = db.folder_get(
    	space_id,
    	folder_id
    )
    fdata.deleted = false
    db.folder_update(
    	space_id,
    	folder_id,
    	fdata
    )
    local children = get_child_folders(
    	space_id,
    	folder_id
    )
    for _, child_id in ipairs(children) do
        restore_recursive(
        	space_id,
        	child_id
        )
    end
end

function save_try(data, user_id)
    local space_id = user_id
    local count = 0
    db.begin()
    local message = check_write_access(
    	space_id,
    	user_id,
    	{}
    )
    if message then
        db.rollback()
        log_user_event(
        	user_id,
        	"save_try-failed",
        	{message=message}
        )
        return false, message, count
    else
        local fields = {
        	type = data.type,
        	name = data.name,
        	background = data.background,
        	diaLine = data.diaLine,
        	diaLineThickness = data.diaLineThickness,
        	font = data.font
        }
        local ok, folder_id, dcount = create_folder_kernel(
        	space_id,
        	fields,
        	user_id,
        	current_version,
        	root_folder_id
        )
        if ok then
            insert_items_from_map(
            	space_id,
            	folder_id,
            	data.free
            )
            insert_items_from_map(
            	space_id,
            	folder_id,
            	data.nodes
            )
            insert_items_from_map(
            	space_id,
            	folder_id,
            	data.edges
            )
            db.commit()
            return true, folder_id, 0
        else
            message = folder_id
            count = dcount
            db.rollback()
            log_user_event(
            	user_id,
            	"save_try-failed",
            	{message=message}
            )
            return false, message, count
        end
    end
end

function seach_text_chunk(item_id, text, needle, found)
    local plain = utils.remove_html_tags(
    	text
    )
    if norm_contains(plain, needle) then
        local match = {
        	item_id = item_id,
        	text = plain
        }
        table.insert(found, match)
        return true
    else
        return false
    end
end

function search_add(session_id, search)
    search_delete(session_id)
    globs.searches[session_id] = search
end

function search_delete(session_id)
    local search = globs.searches[session_id]
    if search then
        search.state = nil
        globs.searches[session_id] = nil
    end
end

function search_items_get(session_id)
    local search = get_search(session_id)
    if search then
        local result = {}
        search:get(result)
        return result
    else
        return nil
    end
end

function search_items_start(session_id, space_id, needle, user_id, roles)
    ej.info(
    	"search_items_start",
    	{user_id=user_id, needle=needle,
    	space_id=space_id}
    )
    local space_error, a, p = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if space_error then
        return false, space_error
    else
        local diagrams = get_space_diagrams(
        	space_id
        )
        local msg = {
        	space_id = space_id,
        	needle = needle,
        	session_id = session_id,
        	diagrams = diagrams
        }
        local search = search_machine()
        search:update(msg)
        local run_search = function ()
        	search_loop(search)
        end
        db.run_in_fiber(
        	run_search,
        	"search"
        )
        return true, {}
    end
end

function search_loop(search)
    while true do
        fiber.sleep(search_yield)
        if search.state then
            
        else
            break
        end
        local now = os.time()
        if now > search.expiry then
            break
        end
        search:update(nil)
    end
end

function search_machine_completed_default(self, msg)
    self.state = "completed"
end

function search_machine_completed_get(self, msg)
    take_found_items(self, msg)
    msg.completed = true
    search_delete(self.session_id)
    self.state = nil
end

function search_machine_init_default(self, msg)
    self.state = "init"
end

function search_machine_init_update(self, msg)
    self.items = {}
    self.session_id = msg.session_id
    self.needle = utils.build_needle(msg.needle)
    self.diagrams = msg.diagrams
    self.current = 1
    extend_life(self)
    search_add(
    	self.session_id,
    	self
    )
    self.state = "scanning"
end

function search_machine_scanning_get(self, msg)
    take_found_items(self, msg)
    msg.completed = false
    extend_life(self)
    self.state = "scanning"
end

function search_machine_scanning_update(self, msg)
    if self.current > #self.diagrams then
        self.state = "completed"
    else
        local diagram = self.diagrams[self.current]
        search_scan_items(
        	diagram,
        	self.needle,
        	self.items
        )
        self.current = self.current + 1
        self.state = "scanning"
    end
end

function search_scan_items(diagram, needle, result)
    local space_id = diagram.space_id
    local folder_id = diagram.folder_id
    local fields = diagram.fields
    local found = {}
    local items = db.item_get_by_folder(
    	space_id,
    	folder_id
    )
    if fields.params then
        seach_text_chunk(
        	"params",
        	fields.params,
        	needle,
        	found
        )
    end
    for _, item_row in ipairs(items) do
        local item_id = item_row[3]
        local item = item_row[4]
        if item.role == "header" then
            
        else
            local text = get_item_text(item)
            local text2 = get_item_text2(item)
            local secondary = get_item_secondary(item)
            if (seach_text_chunk(item_id, text, needle, found)) or (seach_text_chunk(item_id, text2, needle, found)) then
                
            else
                seach_text_chunk(item_id, secondary, needle, found)
            end
        end
    end
    if #found == 0 then
        
    else
        local path = get_short_path(
        	space_id,
        	folder_id
        )
        for _, match in ipairs(found) do
            match.space_id = space_id
            match.folder_id = folder_id
            match.name = fields.name
            match.type = fields.type
            match.path = path
            table.insert(result, match)
        end
    end
end

function set_folder_props(space_id, folder_id, props, user_id, roles)
    db.begin()
    local ok, space_error = can_edit_folder(
    	space_id,
    	folder_id,
    	user_id,
    	roles
    )
    if ok then
        db.folder_props_delete(
        	space_id,
        	folder_id
        )
        for name, value in pairs(props) do
            db.folder_props_insert(
            	space_id,
            	folder_id,
            	name,
            	value
            )
        end
        db.commit()
        return nil
    else
        db.rollback()
        return space_error
    end
end

function set_space_limits(space_id, max_diagrams, max_users)
    local sdata = db.space_get(space_id)
    sdata.max_diagrams = max_diagrams
    sdata.max_users = max_users
    db.space_update(
    	space_id,
    	sdata
    )
end

function set_theme(user_id, settings)
    for key, value in pairs(settings) do
        db.user_props_upsert(
        	user_id,
        	key,
        	value
        )
    end
end

function sort_for_move(items)
    local add_depth = function(item)
    	local depth = calculate_depth(
    		item.space_id,
    		item.id
    	)
    	return {
    		id = item.id,
    		space_id = item.space_id,
    		depth = depth
    	}
    end
    local result = fun.totable(fun.map(
    	add_depth,
    	items
    ))
    local compare_depth = function(left, right)
    	return left.depth > right.depth
    end
    table.sort(
    	result,
    	compare_depth
    )
    return result
end

function space_exists(space_id, user_id, roles)
    local sdata = db.space_get(space_id)
    if sdata then
        return true
    else
        return false
    end
end

function take_found_items(self, dst)
    dst.items = self.items
    self.items = {}
end

function top_spaces()
    local spaces = db.space_get_all()
    local result = {}
    for _, space_row in ipairs(spaces) do
        local space_id = space_row[1]
        local admin = get_first_admin(space_id)
        local email = db.user_get_email(admin) or ""
        local count = get_diagram_count(space_id)
        local item = {
        	space_id = space_id,
        	user = admin,
        	email = email,
        	count = count
        }
        table.insert(result, item)
    end
    table.sort(result, by_count)
    local txt = ""
    for _, item in ipairs(result) do
        txt = txt .. item.space_id .. "\t"
          .. item.user .. "\t"
          .. item.email .. "\t"
          .. item.count .. "\n"
    end
    return txt
end

function update_folder(space_id, folder_id, fields, user_id, roles)
    db.begin()
    local message
    local ok, result = can_edit_folder(
    	space_id,
    	folder_id,
    	user_id,
    	roles
    )
    if ok then
        local fdata = result
        local old_parent = db.folder_tree_get(
        	space_id,
        	folder_id
        )
        local tag = make_folder_tag()
        remember_update(fdata, user_id)
        copyNotNil(fields, "name", fdata)
        copyNotNil(fields, "tag", fdata)
        copyNotNil(fields, "background", fdata)
        copyNotNil(fields, "font", fdata)
        copyNotNil(fields, "description", fdata)
        copyNotNil(fields, "keywords", fdata)
        copyNotNil(fields, "params", fdata)
        local new_parent = extract_parent_id(
        	fields
        )
        if (new_parent) and (not (new_parent == old_parent)) then
            if find_cycle(space_id, folder_id, new_parent) then
                message = "ERR_CYCLE"
                db.rollback()
                return message
            else
                message = add_child(
                	space_id,
                	new_parent,
                	folder_id,
                	user_id
                )
                if message then
                    db.rollback()
                    return message
                else
                    fdata.tag = tag
                    remember_update(fdata, user_id)
                    db.folder_update(
                    	space_id,
                    	folder_id,
                    	fdata
                    )
                    db.commit()
                    return nil
                end
            end
        else
            fdata.tag = tag
            remember_update(fdata, user_id)
            db.folder_update(
            	space_id,
            	folder_id,
            	fdata
            )
            db.commit()
            return nil
        end
    else
        message = result
        db.rollback()
        return message
    end
end

function update_folder_tag(space_id, folder_id, tag, user_id)
    local fdata = db.folder_get(space_id, folder_id)
    remember_update(fdata, user_id)
    fdata.tag = tag
    db.folder_update(
    	space_id,
    	folder_id,
    	fdata
    )
end

function update_item(space_id, folder_id, item_id, new_data, deleted)
    local idata = db.item_get(
    	space_id,
    	folder_id,
    	item_id
    )
    if idata then
        for key, value in pairs(new_data) do
            idata[key] = value
        end
        db.item_update(
        	space_id,
        	folder_id,
        	item_id,
        	idata
        )
    else
        log.error("item not found " .. item_id)
        if deleted[item_id] then
            
        else
            for key, value in pairs(new_data) do
                idata[key] = value
            end
            db.item_update(
            	space_id,
            	folder_id,
            	item_id,
            	idata
            )
        end
    end
end

function update_space_limits(space_id)
    local rights = db.rights_get_by_space(
    	space_id
    )
    local admins = get_admins(rights)
    update_space_limits_core(
    	space_id,
    	admins
    )
end

function update_space_limits_core(space_id, admins)
    local max_users = 0
    local max_diagrams = 0
    for _, user_id in ipairs(admins) do
        local ulimits = get_user_limits(user_id)
        max_users = math.max(
        	max_users,
        	ulimits.max_users
        )
        max_diagrams = math.max(
        	max_diagrams,
        	ulimits.max_diagrams
        )
    end
    set_space_limits(
    	space_id,
    	max_diagrams,
    	max_users
    )
end

function write_index_js(mods, jsname)
    local instances = {}
    for _, mod in ipairs(mods) do
        instances[mod.name] = "_inst_" .. mod.name
    end
    local src = "(function() {\n"
    for _, mod in ipairs(mods) do
        local inst = instances[mod.name]
        src = src .. "var " .. inst .. " = " ..
          mod.name .. "();\n"
    end
    for _, mod in ipairs(mods) do
        src = resolve_deps(src, mod, instances)
    end
    local startup = find_startup(mods)
    src = src .. instances[startup] .. ".main();\n"
    src = src .. "})();\n"
    utils.write_all_bytes(jsname, src)
end


function search_machine()
    local obj = {}
    obj.type_name = "search_machine"
    obj.state = "init"
    obj.get = function(self, msg)
        local _state_ = self.state
        if _state_ == "init" then
            return search_machine_init_default(self, msg)
        elseif _state_ == "scanning" then
            return search_machine_scanning_get(self, msg)
        elseif _state_ == "completed" then
            return search_machine_completed_get(self, msg)
        end
        return nil
    end
    obj.update = function(self, msg)
        local _state_ = self.state
        if _state_ == "init" then
            return search_machine_init_update(self, msg)
        elseif _state_ == "scanning" then
            return search_machine_scanning_update(self, msg)
        elseif _state_ == "completed" then
            return search_machine_completed_default(self, msg)
        end
        return nil
    end
    return obj
end

return {
	create_space = create_space,
	get_folder = get_folder,
	get_tag = get_tag,
	get_theme = get_theme,
	set_theme = set_theme,
	edit = edit,
	create_folder = create_folder,
	update_folder = update_folder,
	delete_many = delete_many,
	copy_many = copy_many,
	move_many = move_many,
	get_trash = get_trash,
	restore = restore,
	clear_trash = clear_trash,
	get_tree = get_tree,
	access = access,
	get_recent = get_recent,
	save_try = save_try,
	read_access = read_access,
	space_exists = space_exists,
	multi_access = multi_access,
	delete_space = delete_space,
	set_space_limits = set_space_limits,
	update_space_limits = update_space_limits,
	get_user_limits = get_user_limits,
	delete_recent = delete_recent,
	delete_theme = delete_theme,
	remove_user = remove_user,
	def_query = def_query,
	get_space_access = get_space_access,
	export_space = export_space,
	import_space = import_space,
	find_folders = find_folders,
	search_delete = search_delete,
	search_items_start = search_items_start,
	search_items_get = search_items_get,
	top_spaces = top_spaces,
	get_folder_props = get_folder_props,
	set_folder_props = set_folder_props,
	get_create_gentoken = get_create_gentoken,
	reset_gentoken = reset_gentoken,
	get_prog_modules = get_prog_modules,
	get_module = get_module,
	check_write_access = check_write_access,
	backup = backup,
	restore_backup = restore_backup,
	get_modules = get_modules,
	genapp = genapp,
	downloadapp = downloadapp,
	get_modules_for_app = get_modules_for_app,
	find_folder_by_name = find_folder_by_name
}
