-- 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 utf8 = require("lua-utf8")

local fio = require("fio")

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

local db = require(global_cfg.db)

setfenv(1, {}) 

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

function access_core(data, grant, user_id, admin)
    local count = 0
    local space_id = data.workspace
    local message = check_admin_access(
    	space_id,
    	user_id,
    	admin
    )
    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 (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 fdata.type == "folder" then
        db.folder_tree_upsert(
        	space_id,
        	child_id,
        	folder_id
        )
        return nil
    else
        return "ERR_NOT_FOLDER"
    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 fdata.type == "folder" 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)
    log.info("backup " .. space_id)
    log_user_event(
    	user_id,
    	"backup",
    	{space = space_id}
    )
    local ok, data = prepare_backup(
    	space_id,
    	user_id,
    	roles
    )
    if ok then
        local names = data
        backup_project(space_id, names.folder)
        local command = "cd " .. names.folder .. "; zip -r " .. space_id ..
        	".zip " .. space_id
        log.info(command)
        local cmd_result = os.execute(command)
        log.info(cmd_result)
        local down_name = space_id .. ".zip"
        result = {
        	filename = down_name,
        	url = names.url
        }
        schedule_delete(names.folder)
        return true, result
    else
        log.error("backup: " .. data)
        return false, data
    end
end

function backup_folder(space_id, folder_id, output)
    local fdata = find_folder(space_id, folder_id)
    if fdata then
        log.info("backup folder " .. space_id
         .. "/" .. folder_id .. " " .. fdata.name)
        local name = unique_filename(
        	fdata.name,
        	output
        )
        fdata.updated_by = nil
        fdata.created_by = nil
        fdata.when_created = nil
        fdata.when_updated = nil
        fdata.tag = nil
        fdata.children = {}
        local low =  utf8.lower(name)
        fdata.name = name
        output[low] = fdata
        fdata.items = get_items(space_id, folder_id)
        local kids = get_child_folders(
        	space_id,
        	folder_id
        )
        for _, child_id in ipairs(kids) do
            backup_folder(
            	space_id,
            	child_id,
            	fdata.children
            )
        end
    end
end

function backup_project(space_id, path)
    local tmp = {}
    local output = {}
    backup_folder(space_id, "1", tmp)
    for bad, folder in pairs(tmp) do
        folder.name = space_id
        output[space_id] = folder
    end
    save_folders(output, path)
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, admin)
    local message = check_write_access(
    	space_id,
    	user_id,
    	admin
    )
    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, admin)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	admin
        )
        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, admin)
    local message = check_write_access(
    	space_id,
    	user_id,
    	admin
    )
    if message then
        return message
    else
        local fdata = find_folder(
        	space_id,
        	folder_id
        )
        if fdata then
            if fdata.type == "folder" 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, admin)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	admin
        )
        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_write_access(space_id, user_id, admin)
    local sdata = db.space_get(space_id)
    if sdata then
        local access = get_access(
        	sdata,
        	space_id,
        	user_id,
        	admin
        )
        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, admin)
    local message = check_write_access(
    	space_id,
    	user_id,
    	admin
    )
    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
            )
        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
        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, admin)
    local ok, message
    message = check_dst_folder(
    	dst_space_id,
    	dst_folder_id,
    	user_id,
    	admin
    )
    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,
            	admin
            )
            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, admin)
    local count = 0
    db.begin()
    local message = check_write_access(
    	space_id,
    	user_id,
    	admin
    )
    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 (fields.type == "folder")) then
        return false, "ERR_DIAGRAM_LIMIT", diagrams + 1
    else
        local id = sdata.next_id
        sdata.next_id = id + 1
        local folder_id = tostring(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_space(space_id, owner)
    local now = clock.time()
    local sdata = {
    	public = false,
    	when_created = now,
    	when_updated = now,
    	next_id = 2
    }
    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, admin)
    local space_error, access, is_public = check_read_access(
    	data.space_id,
    	user_id,
    	admin
    )
    if space_error then
        return false, space_error
    else
        local lines = {}
        for _, line in ipairs(data.lines) do
            local normalized = utils.normalize_string(line)
            table.insert(lines, normalized)
        end
        local found = {}
        local match = function(sid, fid, folder)
        	folder_matches_reversed(
        		sid, fid, folder, 
        		lines, found
        	)
        end
        for_space_folders(data.space_id, match)
        return true, {
        	items = found
        }
    end
end

function delete_folder(space_id, folder_id)
    db.folder_tree_delete(
    	space_id,
    	folder_id
    )
    remove_from_recent(
    	space_id,
    	folder_id
    )
    remove_from_trash(
    	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(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, admin)
    local message
    local deleted = {}
    db.begin()
    for _, item in ipairs(items) do
        message = delete_one(
        	item.space_id,
        	item.id,
        	user_id,
        	admin,
        	deleted,
        	true
        )
        if message then
            db.rollback()
            return message
        end
    end
    db.commit()
    return nil
end

function delete_one(space_id, folder_id, user_id, admin, 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,
        	admin
        )
        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,
                	admin,
                	deleted,
                	false
                )
                if message then
                    break
                end
            end
        else
            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_space(space_id, user_id, admin)
    local message = check_admin_access(
    	space_id,
    	user_id,
    	admin
    )
    if message then
        
    else
        local space = db.space_get(space_id)
        db.rights_delete_by_space(space_id)
        delete_space_folders(space_id)
        db.space_delete(space_id)
        log_user_event(
        	user_id,
        	"vud_delete_space",
        	{space_id=space_id}
        )
    end
    return message
end

function delete_space_folders(space_id)
    local folders = db.folder_get_by_space(
    	space_id
    )
    for _, row in ipairs(folders) do
        local folder_id = row[2]
        delete_folder(space_id, folder_id)
    end
end

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

function edit(space_id, folder_id, data, user_id, admin)
    db.begin()
    local message = nil
    local ok, result = can_edit_folder(
    	space_id,
    	folder_id,
    	user_id,
    	admin
    )
    if ok then
        local fdata = result
        if fdata.tag == data.oldTag then
            copyNotNil(data, "name", fdata)
            copyNotNil(data, "style", 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)
    local start = find_body_start(payload)
    local 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)
    local i, b1, b2
    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)
    local 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, admin)
    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,
    	admin
    )
    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, admin)
    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,
    	admin
    )
    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 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 get_access(sdata, space_id, user_id, admin)
    if admin then
        return "admin"
    else
        if user_id == "" then
            if sdata.public 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 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 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 then
            local item = {
            	space_id = space_id,
            	id = child_id,
            	type = cdata.type,
            	name = cdata.name
            }
            table.insert(children, item)
        end
    end
    return children
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 (fdata.type == "folder") or (fdata.deleted) then
            
        else
            count = count + 1
        end
    end
    return count
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, admin)
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	admin
    )
    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 (fdata.type == "folder") then
                
            else
                add_to_recent(
                	space_id,
                	folder_id,
                	user_id
                )
            end
            local path = 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
            )
            local result = {
            	id = folder_id,
            	space_id = space_id,
            	name = fdata.name,
            	background = fdata.background,
            	diaLine = fdata.diaLine,
            	diaLineThickness = fdata.diaLineThickness,
            	font = fdata.font,
            	style = fdata.style,
            	type = fdata.type,
            	tag = fdata.tag,
            	parent = parent,
            	description = fdata.description,
            	access = access,
            	children = children,
            	items = items,
            	path = path,
            	is_public = is_public,
            	version = fdata.version or 0
            }
            return true, result
        else
            return false, "ERR_NOT_FOUND"
        end
    end
end

function get_item_text(item)
    local content = item.content
    if content then
        return content.txt
    else
        return ""
    end
end

function get_item_text2(item)
    local content = item.content
    if content then
        return content.txt2
    else
        return ""
    end
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_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_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 = {}
    while true do
        local fdata = db.folder_get(
        	space_id,
        	folder_id
        )
        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
    return utils.reverse(path)
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
            )
            record.name = folder.name
            record.type = folder.type
            record.path = get_path(
            	record.space_id,
            	record.folder_id
            )
            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, admin)
    local space_error, access, is_public = check_read_access(
    	space_id,
    	user_id,
    	admin
    )
    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_theme(user_id)
    local row = db.usettings_get(user_id)
    if row then
        return true, row
    else
        return true, {}
    end
end

function get_trash(space_id, user_id, admin)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	admin
    )
    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, admin)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	admin
    )
    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"
    )
    local obj = utils.read_json(filename)
    local min_id = 1
    for _, folder in ipairs(obj.folders) do
        local id = tonumber(folder.folder_id)
        min_id = math.max(min_id, id)
    end
    local next_id = min_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
    local sdata = db.space_get(space_id)
    sdata.next_id = next_id
    db.space_update(
    	space_id,
    	sdata
    )
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 fields.type == "folder" 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 load_diagram(path, output, context)
    local name = fio.basename(path, ".json")
    local content = utils.read_all_bytes(path)
    local diagram = json.decode(content)
    diagram.name = name
    table.insert(output, diagram)
end

function load_dir(path, output, context)
    log.info("load_dir " .. path)
    local name = fio.basename(path)
    local fdata = {
    	type = "folder",
    	name = name,
    	children = {}
    }
    table.insert(output, fdata)
    load_project(path, fdata.children, context)
end

function load_project(folder, output, context)
    local files = fio.listdir(folder)
    for _, file in ipairs(files) do
        local path = folder .. "/" .. file
        if fio.path.is_dir(path) then
            load_dir(path, output, context)
        else
            if utils.ends_with(file, ".json") then
                load_diagram(path, output, context)
            end
        end
    end
    return true
end

function log_user_event(user_id, type, data)
    data.user_id = user_id
    ej.info(type, data)
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 folder.type == "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, admin)
    local message = check_dst_folder(
    	dst_space_id,
    	dst_folder_id,
    	user_id,
    	admin
    )
    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,
            	admin
            )
            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, admin)
    for _, block in ipairs(data.blocks) do
        local grant = (block.action == "grant")
        local message, count = access_core(
        	block,
        	grant,
        	user_id,
        	admin
        )
        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 norm_contains(haystack, needle)
    if haystack then
        local norm_haystack = utils.normalize_string(
        	haystack
        )
        return utils.string_contains(
        	norm_haystack,
        	needle
        )
    else
        return false
    end
end

function normalize_filename(raw)
    local bad = {}
    local result = ""
    local ch
    bad["/"] = true
    bad["\\"] = true
    bad["\""] = true
    bad["\'"] = true
    bad["?"] = true
    bad["*"] = true
    bad["<"] = true
    bad[">"] = true
    bad["|"] = true
    bad[":"] = true
    for i, code in utf8.next, raw do
        if ((code == 9) or (code == 10)) or (code == 13) then
            ch = " "
        else
            if code < 32 then
                ch = "_"
            else
                ch = utf8.char(code)
                if bad[ch] then
                    ch = "_"
                end
            end
        end
        result = utf8.insert(
        	result,
        	ch
        )
    end
    if #result == 0 then
        return "Bad-name"
    else
        return result
    end
end

function prepare_backup(space_id, user_id, roles)
    local message = check_read_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        return false, message
    else
        local gentoken = utils.random_password(16)
        local path = global_cfg.files ..
          "/" .. gentoken
        if fio.mkdir(path) then
            local names = {}
            local filename = space_id .. ".zip"
            names.url = "/files/" .. gentoken ..
            	"/" .. filename
            names.filename = path .. "/" .. filename
            names.folder = path
            return true, names
        else
            return false, "ERROR_COULD_NOT_CREATE_FOLDER"
        end
    end
end

function read_access(space_id, user_id, admin)
    local space_error, access = check_read_access(
    	space_id,
    	user_id,
    	admin
    )
    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
        )
        log.info("deleted recent: " .. folder_id)
    end
end

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

function restore(space_id, folder_id, user_id, admin)
    db.begin()
    local message = check_write_access(
    	space_id,
    	user_id,
    	admin
    )
    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)
    local names
    log.info("restore_backup " .. space_id)
    log_user_event(
    	user_id,
    	"restore_backup",
    	{space = space_id}
    )
    local message = check_write_access(
    	space_id,
    	user_id,
    	roles
    )
    if message then
        data = message
        return false, data
    else
        local ok, data = prepare_backup(
        	space_id,
        	user_id,
        	roles
        )
        if ok then
            names = data
            local content = extract_multipart_body(body)
            utils.write_all_bytes(names.filename, content)
            local command = "cd " .. names.folder .. "; unzip " .. 
            	space_id .. ".zip"
            log.info(command)
            local cmd_result = os.execute(command)
            log.info(cmd_result)
            if cmd_result == 0 then
                local context = {}
                local diagrams = {}
                load_project(
                	names.folder,
                	diagrams,
                	context
                )
                if context.error then
                    return false, "Errors in structure"
                else
                    delete_space_folders(space_id)
                    local sdata = db.space_get(space_id)
                    local now = clock.time()
                    sdata.next_id = 2
                    sdata.when_updated = now
                    db.space_update(
                    	space_id,
                    	sdata
                    )
                    local root = diagrams[1]
                    local children = root.children
                    root.name = "<root>"
                    root.children = nil
                    db_create_folder(
                    	space_id,
                    	root_folder_id,
                    	root,
                    	user_id,
                    	current_version,
                    	nil
                    )
                    restore_children(
                    	children,	
                    	space_id,
                    	root_folder_id,
                    	user_id
                    )
                    schedule_delete(names.folder)
                    return true, {}
                end
            else
                return false, "Could not unzip"
            end
        else
            return false, data
        end
    end
end

function restore_children(children, space_id, parent_id, user_id)
    for _, child in ipairs(children) do
        if child.type == "folder" then
            restore_folder(
            	child,
            	space_id,
            	parent_id,
            	user_id
            )
        else
            restore_diagram(
            	child,
            	space_id,
            	parent_id,
            	user_id
            )
        end
    end
end

function restore_diagram(fdata, space_id, parent_id, user_id)
    local items = fdata.items
    fdata.children = nil
    fdata.items = nil
    local ok, folder_id = create_folder_kernel(
    	space_id,
    	fdata,
    	user_id,
    	current_version,
    	parent_id
    )
    if items then
        for _, item in ipairs(items) do
            local itemId = item.id
            item.id = nil
            db.item_insert(
            	space_id,
            	folder_id,
            	itemId,
            	item
            )
        end
    end
end

function restore_folder(fdata, space_id, parent_id, user_id)
    local children = fdata.children
    fdata.children = nil
    local ok, folder_id = create_folder_kernel(
    	space_id,
    	fdata,
    	user_id,
    	current_version,
    	parent_id
    )
    if children then
        restore_children(
        	children,
        	space_id,
        	folder_id,
        	user_id
        )
    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_diagram(diagram, path)
    local filename = path .. "/" .. diagram.name .. ".json"
    log.info("save diagram " .. diagram.name
     .. " " .. filename)
    diagram.name = nil
    diagram.children = nil
    utils.write_json(filename, diagram)
end

function save_folder(folder, path)
    local full_path = path .. "/" .. folder.name
    log.info("save folder " .. folder.name
     .. " " .. full_path)
    if fio.mkdir(full_path) then
        
    else
        log.error("Could not create folder " .. full_path)
    end
end

function save_folders(folders, path)
    for low, folder in pairs(folders) do
        if folder.type == "folder" then
            save_folder(folder, path)
            local cpath = path .. "/".. folder.name
            save_folders(folder.children, cpath)
        else
            save_diagram(folder, path)
        end
    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,
    	false
    )
    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 schedule_delete(filename)
    log.info("schedule_delete " .. filename)
    local deleter = function()
    	fiber.sleep(1200)
    	local command = "rm -rf " .. filename
    	log.info(command)
    	local cmd_result = os.execute(command)
    	log.info(cmd_result)
    end
    fiber.create(deleter)
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, admin)
    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,
    	admin
    )
    if space_error then
        return false, space_error
    else
        local norm_needle = utils.normalize_string(
        	needle
        )
        local diagrams = get_space_diagrams(
        	space_id
        )
        local msg = {
        	space_id = space_id,
        	needle = norm_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 = 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 match
    local found = {}
    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]
        if item.role == "header" then
            
        else
            local text = get_item_text(item)
            local text2 = get_item_text2(item)
            if norm_contains(text, needle) then
                match = {
                	item_id = item_id,
                	text = text
                }
                table.insert(found, match)
            else
                if norm_contains(text2, needle) then
                    match = {
                    	item_id = item_id,
                    	text = text2
                    }
                    table.insert(found, match)
                end
            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_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)
    local data = db.usettings_get(user_id)
    if data then
        
    else
        data = {}
    end
    copyNotNil(settings, "theme", data)
    copyNotNil(settings, "language", data)
    copyNotNil(settings, "zoom", data)
    copyNotNil(settings, "toolbar", data)
    copyNotNil(settings, "toolbarFree", data)
    copyNotNil(settings, "shape", data)
    copyNotNil(settings, "font", data)
    copyNotNil(settings, "background", data)
    copyNotNil(settings, "diaLine", data)
    copyNotNil(settings, "colors", data)
    copyNotNil(settings, "yes", data)
    copyNotNil(settings, "no", data)
    copyNotNil(settings, "end", data)
    db.usettings_upsert(
    	user_id,
    	data
    )
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, admin)
    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 unique_filename(raw, names)
    local norm, low
    norm = normalize_filename(raw)
    while true do
        low = utf8.lower(norm)
        if names[low] then
            
        else
            break
        end
        norm = norm .. "x"
    end
    return norm
end

function update_folder(space_id, folder_id, fields, user_id, admin)
    db.begin()
    local message
    local ok, result = can_edit_folder(
    	space_id,
    	folder_id,
    	user_id,
    	admin
    )
    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)
        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 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,
	find_folder_by_name = find_folder_by_name,
	backup = backup,
	restore_backup = restore_backup
}
