<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_cns
 */

/**
 * Find whether a member can alter a poll owned by a certain member in a certain forum.
 *
 * @param  ?AUTO_LINK $forum_id The forum (null: private topics)
 * @param  MEMBER $poll_owner The poll owner
 * @param  ?MEMBER $member_id The member we are checking for (null: current member)
 * @return boolean The answer
 */
function cns_may_edit_poll_by(?int $forum_id, int $poll_owner, ?int $member_id = null) : bool
{
    if ($member_id === null) {
        $member_id = get_member();
    }

    if (has_privilege($member_id, 'edit_midrange_content', 'topics', ['forums', $forum_id])) {
        return true;
    }

    if ((has_privilege($member_id, 'edit_own_polls', 'topics', ['forums', $forum_id])) && ($member_id == $poll_owner)) {
        return true;
    }

    return false;
}

/**
 * Find whether a member may attach a poll to a detailed topic.
 *
 * @param  AUTO_LINK $topic_id The topic
 * @param  ?MEMBER $topic_owner The topic owner (null: ask the DB for it)
 * @param  ?boolean $has_poll_already Whether the topic already has a poll (null: ask the DB for it)
 * @param  ?MEMBER $forum_id The forum the topic is in (null: ask the DB for it)
 * @param  ?MEMBER $member_id The member we are checking for (null: current member)
 * @return boolean The answer
 */
function cns_may_attach_poll(int $topic_id, ?int $topic_owner = null, ?bool $has_poll_already = null, ?int $forum_id = null, ?int $member_id = null) : bool
{
    if ($topic_owner === null) {
        $topic_info = $GLOBALS['FORUM_DB']->query_select('f_topics', ['*'], ['id' => $topic_id], '', 1);
        if (!array_key_exists(0, $topic_info)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }
        $topic_owner = $topic_info[0]['t_cache_first_member_id'];
        $has_poll_already = ($topic_info[0]['t_poll_id'] !== null);
        $forum_id = $topic_info[0]['t_forum_id'];
    }

    if ($member_id === null) {
        $member_id = get_member();
    }
    if ($has_poll_already) {
        return false;
    }

    if (($topic_owner == $member_id) && (!is_guest($member_id))) {
        return true;
    }
    if (cns_may_moderate_forum($forum_id, $member_id)) {
        return true;
    }

    return false;
}

/**
 * Find whether a member can delete a poll owned by a certain member in a certain forum.
 *
 * @param  ?AUTO_LINK $forum_id The forum (null: private topics)
 * @param  MEMBER $poll_owner The poll owner
 * @param  ?MEMBER $member_id The member we are checking for (null: current member)
 * @return boolean The answer
 */
function cns_may_delete_poll_by(?int $forum_id, int $poll_owner, ?int $member_id = null) : bool
{
    require_code('cns_polls_action3');

    if ($member_id === null) {
        $member_id = get_member();
    }

    // If requireTopicPoll is true, disallow deleting a poll from a topic. The entire topic should be deleted instead.
    $default_poll_options = [];
    $default_poll_options = cns_get_default_poll_options($forum_id);
    if ($default_poll_options['requireTopicPoll']) {
        return false;
    }

    if (has_privilege($member_id, 'delete_midrange_content', 'topics', ['forums', $forum_id])) {
        return true;
    }

    if ((has_privilege($member_id, 'delete_own_midrange_content', 'topics', ['forums', $forum_id])) && ($member_id == $poll_owner)) {
        return true;
    }

    return false;
}

/**
 * Find a map of results relating to a certain poll.
 * Applies forfeiting is required.
 *
 * @param  AUTO_LINK $poll_id The poll
 * @param  boolean $request_results Whether we must record that the current member is requesting the results, blocking future voting for them
 * @param  ?array $request_voters An array of [int $start = 0, string $order_by = 'pv_date_time DESC', int $max = 50] if we want to also return specific member IDs and what they voted (null: Do not include specific votes)
 * @param  ?AUTO_LINK $answer_id The poll answer ID to filter by (null: return all answers)
 * @return ?array The map of results (null: could not find poll)
 */
function cns_poll_get_results(int $poll_id, bool $request_results = true, ?array $request_voters = null, ?int $answer_id = null) : ?array
{
    // SQL injection prevention on $request_voters[1] ($order_by)
    $acceptable_orders = ['pv_date_time ASC', 'pv_date_time DESC', 'pv_member_id ASC', 'pv_member_id DESC', 'pv_answer_id ASC', 'pv_answer_id DESC', 'pv_cache_voting_power ASC', 'pv_cache_voting_power DESC'];
    if ($request_voters !== null && array_key_exists(1, $request_voters) && array_search($request_voters[1], $acceptable_orders) === false) {
        log_hack_attack_and_exit('ORDERBY_HACK');
    }

    $poll_info = $GLOBALS['FORUM_DB']->query_select('f_polls', ['*'], ['id' => $poll_id], '', 1);
    if (!array_key_exists(0, $poll_info)) {
        attach_message(do_lang_tempcode('_MISSING_RESOURCE', escape_html(strval($poll_id)), 'poll'), 'warn');
        return null;
    }

    $point_weighting = ((get_option('enable_poll_point_weighting', true) === '1') && ($poll_info[0]['po_point_weighting'] == 1) && addon_installed('points'));

    // Fetch / calculate voting powers for this poll when applicable
    if ($point_weighting) {
        require_code('cns_polls_action2');

        $poll_info[0]['po_cache_voting_power'] = cns_calculate_poll_voting_power($poll_id, false, $poll_info[0]);
    }

    // Get poll answers and cached votes / power for each answer
    $where_map = ['pa_poll_id' => $poll_id];
    if ($answer_id !== null) {
        $where_map['id'] = $answer_id;
    }
    $_answers = $GLOBALS['FORUM_DB']->query_select('f_poll_answers', ['*'], $where_map, 'ORDER BY pa_order');
    $answers = [];
    foreach ($_answers as $_answer) {
        $answer = [];

        $answer['answer'] = $_answer['pa_answer'];
        $answer['id'] = $_answer['id'];
        if ((($request_results) || ($poll_info[0]['po_is_open'] == 0)) && ($poll_info[0]['po_is_private'] == 0)) { // We usually will show the results for a closed poll, but not one still private
            $answer['num_votes'] = $_answer['pa_cache_num_votes'];
            $answer['voting_power'] = $point_weighting ? cns_calculate_answer_voting_power($_answer['id'], false, $_answer) : $_answer['pa_cache_voting_power'];
        }

        $answers[] = $answer;
    }

    // Do a little poll dancing
    $start = (($request_voters !== null) && (array_key_exists(0, $request_voters))) ? $request_voters[0] : 0;
    $max = (($request_voters !== null) && (array_key_exists(2, $request_voters))) ? $request_voters[2] : 50;
    $max_vote_rows = $GLOBALS['FORUM_DB']->query_select_value('f_poll_votes', 'COUNT(*)', ['pv_poll_id' => $poll_id, 'pv_revoked' => 0]);

    $votes = [];
    $total_voting_power = 0.0;

    if ($request_voters !== null) {
        require_code('cns_polls_action2');

        $order_by = (($request_voters !== null) && (array_key_exists(1, $request_voters))) ? $request_voters[1] : 'pv_date_time DESC';

        $vote_rows_select = ['pv_answer_id', 'pv_member_id', 'pv_date_time', 'pv_points_when_voted'];
        $vote_rows_where = ['pv_poll_id' => $poll_id, 'pv_revoked' => 0];
        if ($answer_id !== null) {
            $vote_rows_where['pv_answer_id'] = $answer_id;
        }
        if ($point_weighting) {
            $_vote_rows = $GLOBALS['FORUM_DB']->query_select('f_poll_votes', $vote_rows_select, $vote_rows_where, 'AND pv_answer_id IS NOT NULL ORDER BY ' . $order_by);
        } else {
            $_vote_rows = $GLOBALS['FORUM_DB']->query_select('f_poll_votes', $vote_rows_select, $vote_rows_where, 'AND pv_answer_id IS NOT NULL ORDER BY ' . $order_by, $max, $start);
        }

        $_answer_ids = array_column($answers, 'id');
        $_vote_rows = $GLOBALS['FORUM_DB']->query_select('f_poll_votes', ['*'], $vote_rows_where, 'AND pv_answer_id IS NOT NULL ORDER BY ' . $order_by, $max, $start);
        foreach ($_vote_rows as $vote) {
            $voting_power = 1.0;
            $voting_equation = ['', '', 1.0];
            if ($point_weighting) {
                $voting_power = cns_points_to_voting_power($vote['pv_points_when_voted']);
                $voting_equation = cns_calculate_poll_voting_power_text($vote['pv_points_when_voted']);

                // Add voting power to the total for the poll
                $total_voting_power += $voting_power;

                // Add voting power to the specific answer, if applicable
                $_answer_index = array_search($vote['pv_answer_id'], $_answer_ids);
                if ($_answer_index !== false) {
                    if (!array_key_exists('voting_power', $answers[$_answer_index])) {
                        $answers[$_answer_index]['voting_power'] = $voting_power;
                    } else {
                        $answers[$_answer_index]['voting_power'] += $voting_power;
                    }
                }
            }

            // Add the voter row, if applicable
            if ($request_voters !== null) {
                $answer_index = array_search($vote['pv_answer_id'], $_answer_ids);
                if ($answer_index !== false) {
                    $answer = ((array_key_exists($answer_index, $answers)) && (array_key_exists('answer', $answers[$answer_index]))) ? $answers[$answer_index]['answer'] : do_lang_tempcode('UNKNOWN');
                    $votes[] = [
                        'pv_answer_id' => $vote['pv_answer_id'],
                        'answer' => $answer,
                        'pv_member_id' => $vote['pv_member_id'],
                        'pv_date_time' => $vote['pv_date_time'],
                        'voting_power' => $voting_power,
                        'voting_equation' => $voting_equation,
                    ];
                }
            }
        }
    }

    if ($request_results) {
        if (is_guest()) {
            $voted_already_map = ['pv_poll_id' => $poll_id, 'pv_ip_address' => get_ip_address(), 'pv_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id(), 'pv_revoked' => 0];
        } else {
            $voted_already_map = ['pv_poll_id' => $poll_id, 'pv_member_id' => get_member(), 'pv_revoked' => 0];
        }
        $voted_already = ($GLOBALS['FORUM_DB']->query_select_value_if_there('f_poll_votes', 'pv_member_id', $voted_already_map) !== null);
        if (!$voted_already) {
            $forfeight = !has_privilege(get_member(), 'view_poll_results_before_voting');
            // If view_poll_results_before_voting is not assigned, member may be forfeiting their vote
            if ($forfeight) {
                // Vote forfeit should only happen if voting revocation is not allowed on this poll
                if ($poll_info[0]['po_vote_revocation'] == 0) {
                    // Guests cannot view results if they cannot vote, view_poll_results_before_voting is not set, and vote revocation is off.
                    if (is_guest() && $poll_info[0]['po_guests_can_vote'] == 0) {
                        warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
                    }
                    $GLOBALS['FORUM_DB']->query_insert('f_poll_votes', [
                        'pv_poll_id' => $poll_id,
                        'pv_member_id' => get_member(),
                        'pv_answer_id' => null,
                        'pv_ip_address' => get_ip_address(),
                        'pv_revoked' => 0,
                        'pv_date_time' => time(),
                        'pv_points_when_voted' => 0, // We do not need to know a member's points for forfeits.
                    ]);
                }
            }
        }
    }

    $out = [
        'is_private' => $poll_info[0]['po_is_private'],
        'id' => $poll_info[0]['id'],
        'question' => $poll_info[0]['po_question'],
        'minimum_selections' => $poll_info[0]['po_minimum_selections'],
        'maximum_selections' => $poll_info[0]['po_maximum_selections'],
        'requires_reply' => $poll_info[0]['po_requires_reply'],
        'is_open' => $poll_info[0]['po_is_open'],
        'closing_time' => $poll_info[0]['po_closing_time'],
        'view_member_votes' => $poll_info[0]['po_view_member_votes'],
        'vote_revocation' => $poll_info[0]['po_vote_revocation'],
        'guests_can_vote' => $poll_info[0]['po_guests_can_vote'],
        'point_weighting' => $poll_info[0]['po_point_weighting'],
        'answers' => $answers,
        'votes' => $votes,
        'max_vote_rows' => $max_vote_rows,
        'total_votes' => $poll_info[0]['po_cache_total_votes'],
        'total_voting_power' => $poll_info[0]['po_cache_voting_power'],
    ];

    return $out;
}
