--[[
    Leads — Adds leads for transporting animals to Minetest.
    Copyright © 2023, Silver Sandstone <@SilverSandstone@craftodon.social>

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.
]]


--- Public API functions.
-- @module api


local S = leads.S;


--- An enumerator of object types.
leads.ObjectType =
{
    PLAYER  = 'player';
    ANIMAL  = 'animal';
    MONSTER = 'monster';
    NPC     = 'npc';
    VEHICLE = 'vehicle';
    OTHER   = 'other';
};


--- Overrides the leashable property of entities.
leads.custom_leashable_entities =
{
    ['boats:boat']  = true;
};

--- Overrides the object type of entities.
leads.custom_object_types =
{
    ['boats:boat'] = leads.ObjectType.VEHICLE;
};

--- Overrides the knottable property of nodes.
leads.custom_knottable_nodes =
{
    ['ferns:fern_trunk']                    = true;
    ['ethereal:bamboo']                     = true;
    ['bambooforest:bamboo']                 = true;
    ['advtrains:signal_off']                = true;
    ['advtrains:signal_on']                 = true;
    ['advtrains:retrosignal_off']           = true;
    ['advtrains:retrosignal_on']            = true;
    ['nodes_nature:mahal']                  = true;
    ['hades_furniture:binding_rusty_bars']  = true;
};

--- A table of sound effects for lead events.
leads.sounds =
{
    attach  = {name = 'leads_attach',  gain = 0.5,  pitch = 0.75};
    remove  = {name = 'leads_remove',  gain = 0.5,  pitch = 0.75};
    stretch = {name = 'leads_stretch', gain = 0.25, pitch = 1.25, duration = 2.5};
    snap    = {name = 'leads_break',   gain = 0.75};
};


local weak_key_mt = {__mode = 'k'};
local leads_by_connector_mt =
{
    __mode = 'k';
    __index = function(self, key)
        local result = setmetatable({}, weak_key_mt);
        self[key] = result;
        return result;
    end;
};
leads.leads_by_connector = setmetatable({}, leads_by_connector_mt);


--- Creates a lead between two objects.
-- @param leader   [ObjectRef]            The leader object.
-- @param follower [ObjectRef]            The follower object.
-- @param item     [string|ItemStack|nil] The lead item, if any.
-- @return         [ObjectRef|nil]        The lead object, or nil on failure.
-- @return         [string|nil]           A string describing the error, or nil on success.
function leads.connect_objects(leader, follower, item)
    if leads.util.is_same_object(leader, follower) then
        return nil, S'You cannot leash something to itself.';
    end;

    item = ItemStack(item);
    local item_def = item:get_definition();

    local l_pos = leader:get_pos();
    local f_pos = follower:get_pos();

    if leads.settings.debug then
        minetest.log(debug.traceback(('[Leads] Connecting L:%s to F:%s.'):format(leads.util.describe_object(leader), leads.util.describe_object(follower))));
    end;

    local centre = (l_pos + f_pos) / 2;

    local object = minetest.add_entity(centre, 'leads:lead');
    if not object then
        return nil, S'Failed to create lead.';
    end;

    local entity = object:get_luaentity();
    entity.leader   = leader;
    entity.follower = follower;
    entity:set_item(item);
    entity:update_visuals();
    entity:update_objref_ids();
    entity:notify_connector_added(leader,   true);
    entity:notify_connector_added(follower, false);
    minetest.sound_play(leads.sounds.attach, {pos = centre}, true);
    return object, nil;
end;


--- Checks if the object can be attached to a lead.
-- @param object [ObjectRef] The object to check.
-- @return       [boolean]   true if the object can be attached to a lead.
function leads.is_leashable(object)
    -- All entities allowed in settings:
    if leads.settings.allow_leash_all then
        return true;
    end;

    -- Check settings:
    local obj_type = leads.util.get_object_type(object);
    if not leads.settings['allow_leash_' .. obj_type] then
        return false;
    end;

    -- Get entity:
    local entity = object:get_luaentity();
    if not entity then
        return obj_type == leads.ObjectType.PLAYER;
    end;

    -- Custom leashable:
    local leashable = entity._leads_leashable or leads.custom_leashable_entities[entity.name];
    if leashable ~= nil then
        return leashable;
    end;

    -- Mobs:
    return leads.util.is_mob(object);
end;


--- Checks if the node can have lead knots tied to it.
-- @param name [string]  The name of a node.
-- @return     [boolean] true if the node is knottable.
function leads.is_knottable(name)
    local def = minetest.registered_nodes[name];
    if not def then
        return false;
    end;

    -- Custom knottable:
    local knottable = def._leads_knottable or leads.custom_knottable_nodes[name];
    if knottable ~= nil then
        return knottable;
    end;

    -- Fence:
    if def.drawtype == 'fencelike' or (minetest.get_item_group(name, 'fence') > 0 and not (name:match('.*:fence_rail_.*') or name:match('.*:gate_.*'))) then
        return true;
    end;

    -- Mese post:
    if name:match('.*:mese_post_.*') then
        return true;
    end;

    -- Lord of the Test fences:
    -- (These aren't in group:fence due to a bug.)
    if name:match('^lottblocks:fence_.*') then
        return true;
    end;

    return false;
end;


--- Finds a lead connected to the specified leader.
-- If there are multiple matching leads, one is chosen arbitrarily.
-- @param leader [ObjectRef]     The player or entity to find leads connected to.
-- @return       [ObjectRef|nil] The lead, if any.
function leads.find_lead_by_leader(leader)
    for lead in leads.find_connected_leads(leader, true, false) do
        return lead;
    end;
    return nil;
end;


--- Finds leads connected to the specified object.
-- @param connector       [ObjectRef] The player or entity to find leads connected to.
-- @param accept_leader   [boolean]   Find leads where the specified object is the leader.
-- @param accept_follower [boolean]   Find leads where the specified object is the follower.
-- @return                [function]  An iterator of (lead: ObjectRef, is_leader: boolean).
function leads.find_connected_leads(connector, accept_leader, accept_follower)
    local function _iter()
        for lead in pairs(leads.leads_by_connector[connector]) do
            local entity = lead:get_luaentity();
            if accept_leader and entity.leader and leads.util.is_same_object(entity.leader, connector) then
                coroutine.yield(lead, true);
            elseif accept_follower and entity.follower and leads.util.is_same_object(entity.follower, connector) then
                coroutine.yield(lead, false);
            end;
        end;
    end;

    return coroutine.wrap(_iter);
end;


--- Ties the leader's lead to a post.
-- @param leader [ObjectRef]     The leader whose lead to tie.
-- @param pos    [vector]        Where to tie the knot.
-- @return       [ObjectRef|nil] The knot object, or nil on failure.
function leads.knot(leader, pos)
    pos = vector.round(pos);

    -- Check protection:
    if leads.settings.respect_protection and not minetest.check_player_privs(leader, 'protection_bypass') then
        local name = leader and leader:get_player_name() or '';
        if minetest.is_protected(pos, name) then
            minetest.record_protection_violation(pos, name);
            return nil;
        end;
    end;

    -- Find a lead attached to the player:
    local lead = leads.find_lead_by_leader(leader);
    if not lead then
        return nil;
    end;

    -- Create a knot:
    local knot = leads.add_knot(pos);
    if not knot then
        return nil;
    end;

    -- Play sound:
    minetest.sound_play(leads.sounds.attach, {pos = pos}, true);

    -- Attach the lead to the knot:
    lead:get_luaentity():set_leader(knot);
    return knot;
end;


--- Adds a knot on a fence post, or finds an existing one.
-- @param pos [vector]        Where to tie the knot.
-- @return    [ObjectRef|nil] A new or existing knot, or nil if creating the knot failed.
function leads.add_knot(pos)
    pos = pos:round();

    for __, object in ipairs(minetest.get_objects_in_area(pos, pos)) do
        local entity = object:get_luaentity();
        if entity and entity.name == 'leads:knot' then
            return object;
        end;
    end;

    return minetest.add_entity(pos, 'leads:knot');
end;


--- Checks if the specified object is immobile (cannot be moved with a lead).
-- @param object [ObjectRef|nil] The object to check.
-- @return       [boolean]       true if the object is immobile.
function leads.is_immobile(object)
    local entity = object and object:get_luaentity();
    return entity and entity._leads_immobile or false;
end;


--- Checks if the player is allowed to leash the object, according to ownership and mod settings.
-- @param object [ObjectRef|nil]        The object to check.
-- @param player [ObjectRef|string|nil] The player trying to leash the object.
-- @return       [boolean]              true if the player is allowed to leash the object.
function leads.allowed_to_leash(object, player)
    local name = '';
    if player == nil then
        name = '';
    elseif type(player) == 'string' then
        name = player;
    else
        name = player:get_player_name() or '';
    end;

    -- Players with the 'protection_bypass' privilege can bypass protection and ownership:
    if minetest.check_player_privs(name, 'protection_bypass') then
        return true;
    end;

    -- Players can always leash their own animals:
    local owner = leads.util.get_object_owner(object);
    if owner == name then
        return true;
    end;

    -- Players can't leash anything else in protected areas if protection support is enabled:
    if leads.settings.respect_protection then
        local pos = object:get_pos():round();
        if minetest.is_protected(pos, name) then
            minetest.record_protection_violation(pos, name);
            return false;
        end;
    end;

    -- Otherwise, use the appropriate setting:
    if owner == '' then
        return leads.settings.allow_leash_unowned or not leads.util.is_mob(object);
    else
        return leads.settings.allow_leash_owned_other;
    end;
end;


--- Implements lead item use.
-- @param itemstack     [ItemStack]     The player's held item.
-- @param user          [ObjectRef]     The player using the lead.
-- @param pointed_thing [PointedThing]  The pointed-thing.
-- @param is_punch      [boolean]       true if the interaction is a punch.
-- @return              [ItemStack|nil] The leftover itemstack, or nil for no change.
function leads.on_lead_interact(itemstack, user, pointed_thing, is_punch)
    local function _message(message)
        if leads.settings.chat_messages then
            minetest.chat_send_player(user:get_player_name(), message);
        end;
    end;


    if pointed_thing.under then
        -- Clicking on a node:
        local pos = pointed_thing.under;
        local node = minetest.get_node(pos);
        if not leads.is_knottable(node.name) then
            return nil;
        end;

        -- Check protection:
        if leads.settings.respect_protection and not minetest.check_player_privs(user, 'protection_bypass') then
            local name = user and user:get_player_name() or '';
            if minetest.is_protected(pos, name) then
                minetest.record_protection_violation(pos, name);
                return nil;
            end;
        end;

        -- Create new lead with knot:
        local knot = leads.add_knot(pos);
        if not knot then
            return nil;
        end;
        leads.connect_objects(user, knot, itemstack:peek_item());

    else
        -- Clicking on an object:
        local object = pointed_thing.ref;
        if not object then
            return nil;
        end;

        -- Try the entity's custom lead interact callback:
        local entity = object:get_luaentity();
        if entity and entity._leads_on_interact then
            local override, result = entity:_leads_on_interact(itemstack, user, pointed_thing, is_punch);
            if override then
                return result;
            end;
        end;

        -- The player right-clicked on a knot — try knotting their lead before making a new one:
        if entity and entity.name == 'leads:knot' then
            if leads.knot(user, object:get_pos()) then
                return nil;
            end;
        end;

        -- Make sure the object is leashable:
        if not leads.is_leashable(object) then
            _message(S'You cannot leash this.');
            return nil;
        end;

        -- Make sure the player is allowed to leash the object:
        if not leads.allowed_to_leash(object, user) then
            _message(S'You do not own this.');
            return nil;
        end;

        -- Create the lead:
        local lead, message = leads.connect_objects(user, pointed_thing.ref, itemstack:peek_item());
        if not lead then
            _message(message);
            return nil;
        end;
    end;

    -- Consume the lead item:
    if not (minetest.is_player(user) and minetest.is_creative_enabled(user:get_player_name())) then
        itemstack:take_item(1);
    end;
    return itemstack;
end;


--- The `on_secondary_use`/`on_rightclick` handler for lead items.
-- @param itemstack     [ItemStack]     The player's held item.
-- @param user          [ObjectRef]     The player using the lead.
-- @param pointed_thing [PointedThing]  The pointed-thing.
-- @return              [ItemStack|nil] The leftover itemstack, or nil for no change.
function leads.on_lead_use(itemstack, user, pointed_thing)
    local result = leads.on_lead_interact(itemstack, user, pointed_thing, false);
    if (not result) and pointed_thing.under then
        -- Fallback to the node's right-click handler:
        local node = minetest.get_node(pointed_thing.under);
        local def = minetest.registered_nodes[node.name] or {};
        return def.on_rightclick and def.on_rightclick(pointed_thing.under, node, user, itemstack, pointed_thing) or nil;
    end;
    return result;
end;


--- The `on_use` handler for lead items.
-- @param itemstack     [ItemStack]     The player's held item.
-- @param user          [ObjectRef]     The player using the lead.
-- @param pointed_thing [PointedThing]  The pointed-thing.
-- @return              [ItemStack|nil] The leftover itemstack, or nil for no change.
function leads.on_lead_punch(itemstack, user, pointed_thing)
    return leads.on_lead_interact(itemstack, user, pointed_thing, true);
end;
