<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * A plugin to enable social-bookmarking functionality
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  PollPlugin
 * @package   StatusNet
 * @author    Brion Vibber <brion@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Poll plugin main class
 *
 * @category  PollPlugin
 * @package   StatusNet
 * @author    Brion Vibber <brionv@status.net>
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class PollPlugin extends MicroAppPlugin
{
    const PLUGIN_VERSION = '0.1.1';

    // @fixme which domain should we use for these namespaces?
    const POLL_OBJECT = 'http://activityschema.org/object/poll';
    const POLL_RESPONSE_OBJECT = 'http://activityschema.org/object/poll-response';

    public $oldSaveNew = true;

    /**
     * Database schema setup
     *
     * @return boolean hook value; true means continue processing, false means stop.
     * @see ColumnDef
     *
     * @see Schema
     */
    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('poll', Poll::schemaDef());
        $schema->ensureTable('poll_response', Poll_response::schemaDef());
        $schema->ensureTable('user_poll_prefs', User_poll_prefs::schemaDef());
        return true;
    }

    /**
     * Show the CSS necessary for this plugin
     *
     * @param Action $action the action being run
     *
     * @return boolean hook value
     */
    public function onEndShowStyles($action)
    {
        $action->cssLink($this->path('css/poll.css'));
        return true;
    }

    /**
     * Map URLs to actions
     *
     * @param URLMapper $m path-to-action mapper
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    public function onRouterInitialized(URLMapper $m)
    {
        $m->connect('main/poll/new',
                    ['action' => 'newpoll']);

        $m->connect('main/poll/:id',
                    ['action' => 'showpoll'],
                    ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);

        $m->connect('main/poll/response/:id',
                    ['action' => 'showpollresponse'],
                    ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);

        $m->connect('main/poll/:id/respond',
                    ['action' => 'respondpoll'],
                    ['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}']);

        $m->connect('settings/poll',
                    ['action' => 'pollsettings']);

        return true;
    }

    /**
     * Plugin version data
     *
     * @param array &$versions array of version data
     *
     * @return bool true hook value
     * @throws Exception
     */
    public function onPluginVersion(array &$versions): bool
    {
        $versions[] = array('name' => 'Poll',
            'version' => self::PLUGIN_VERSION,
            'author' => 'Brion Vibber',
            'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Poll',
            'rawdescription' =>
            // TRANS: Plugin description.
                _m('Simple extension for supporting basic polls.'));
        return true;
    }

    public function types()
    {
        return array(self::POLL_OBJECT, self::POLL_RESPONSE_OBJECT);
    }

    /**
     * When a notice is deleted, delete the related Poll
     *
     * @param Notice $notice Notice being deleted
     *
     * @return boolean hook value
     */
    public function deleteRelated(Notice $notice)
    {
        $p = Poll::getByNotice($notice);

        if (!empty($p)) {
            $p->delete();
        }

        return true;
    }

    /**
     * Save a poll from an activity
     *
     * @param Activity $activity Activity to save
     * @param Profile $profile Profile to use as author
     * @param array $options Options to pass to bookmark-saving code
     *
     * @return Notice resulting notice
     * @throws Exception if it failed
     */
    public function saveNoticeFromActivity(Activity $activity, Profile $profile, array $options = array())
    {
        // @fixme
        common_log(LOG_DEBUG, "XXX activity: " . var_export($activity, true));
        common_log(LOG_DEBUG, "XXX profile: " . var_export($profile, true));
        common_log(LOG_DEBUG, "XXX options: " . var_export($options, true));

        // Ok for now, we can grab stuff from the XML entry directly.
        // This won't work when reading from JSON source
        if ($activity->entry) {
            $pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll');
            $responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response');
            if ($pollElements->length) {
                $question = '';
                $opts = [];

                $data = $pollElements->item(0);
                foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'question') as $node) {
                    $question = $node->textContent;
                }
                foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'option') as $node) {
                    $opts[] = $node->textContent;
                }
                try {
                    $notice = Poll::saveNew($profile, $question, $opts, $options);
                    common_log(LOG_DEBUG, "Saved Poll from ActivityStream data ok: notice id " . $notice->id);
                    return $notice;
                } catch (Exception $e) {
                    common_log(LOG_DEBUG, "Poll save from ActivityStream data failed: " . $e->getMessage());
                }
            } elseif ($responseElements->length) {
                $data = $responseElements->item(0);
                $pollUri = $data->getAttribute('poll');
                $selection = intval($data->getAttribute('selection'));

                if (!$pollUri) {
                    // TRANS: Exception thrown trying to respond to a poll without a poll reference.
                    throw new Exception(_m('Invalid poll response: No poll reference.'));
                }
                $poll = Poll::getKV('uri', $pollUri);
                if (!$poll) {
                    // TRANS: Exception thrown trying to respond to a non-existing poll.
                    throw new Exception(_m('Invalid poll response: Poll is unknown.'));
                }
                try {
                    $notice = Poll_response::saveNew($profile, $poll, $selection, $options);
                    common_log(LOG_DEBUG, "Saved Poll_response ok, notice id: " . $notice->id);
                    return $notice;
                } catch (Exception $e) {
                    common_log(LOG_DEBUG, "Poll response save fail: " . $e->getMessage());
                    // TRANS: Exception thrown trying to respond to a non-existing poll.
                }
            } else {
                common_log(LOG_DEBUG, "YYY no poll data");
            }
        }
        // If it didn't return before
        throw new ServerException(_m('Failed to save Poll response.'));
    }

    public function activityObjectFromNotice(Notice $notice)
    {
        assert($this->isMyNotice($notice));

        switch ($notice->object_type) {
            case self::POLL_OBJECT:
                return $this->activityObjectFromNoticePoll($notice);
            case self::POLL_RESPONSE_OBJECT:
                return $this->activityObjectFromNoticePollResponse($notice);
            default:
                // TRANS: Exception thrown when performing an unexpected action on a poll.
                // TRANS: %s is the unexpected object type.
                throw new Exception(sprintf(_m('Unexpected type for poll plugin: %s.'), $notice->object_type));
        }
    }

    public function activityObjectFromNoticePollResponse(Notice $notice)
    {
        $object = new ActivityObject();
        $object->id = $notice->uri;
        $object->type = self::POLL_RESPONSE_OBJECT;
        $object->title = $notice->content;
        $object->summary = $notice->content;
        $object->link = $notice->getUrl();

        $response = Poll_response::getByNotice($notice);
        if ($response) {
            $poll = $response->getPoll();
            if ($poll) {
                // Stash data to be formatted later by
                // $this->activityObjectOutputAtom() or
                // $this->activityObjectOutputJson()...
                $object->pollSelection = intval($response->selection);
                $object->pollUri = $poll->uri;
            }
        }
        return $object;
    }

    public function activityObjectFromNoticePoll(Notice $notice)
    {
        $object = new ActivityObject();
        $object->id = $notice->uri;
        $object->type = self::POLL_OBJECT;
        $object->title = $notice->content;
        $object->summary = $notice->content;
        $object->link = $notice->getUrl();

        $poll = Poll::getByNotice($notice);
        if ($poll) {
            // Stash data to be formatted later by
            // $this->activityObjectOutputAtom() or
            // $this->activityObjectOutputJson()...
            $object->pollQuestion = $poll->question;
            $object->pollOptions = $poll->getOptions();
        }

        return $object;
    }

    /**
     * Called when generating Atom XML ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Note that you can only add output of additional XML elements,
     * not change existing stuff here.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param XMLOutputter $out to add elements at end of object
     */
    public function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
    {
        if (isset($obj->pollQuestion)) {
            /**
             * <poll:poll xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
             *   <poll:question>Who wants a poll question?</poll:question>
             *   <poll:option>Option one</poll:option>
             *   <poll:option>Option two</poll:option>
             *   <poll:option>Option three</poll:option>
             * </poll:poll>
             */
            $data = array('xmlns:poll' => self::POLL_OBJECT);
            $out->elementStart('poll:poll', $data);
            $out->element('poll:question', array(), $obj->pollQuestion);
            foreach ($obj->pollOptions as $opt) {
                $out->element('poll:option', array(), $opt);
            }
            $out->elementEnd('poll:poll');
        }
        if (isset($obj->pollSelection)) {
            /**
             * <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
             *                poll="http://..../poll/...."
             *                selection="3" />
             */
            $data = array('xmlns:poll' => self::POLL_OBJECT,
                'poll' => $obj->pollUri,
                'selection' => $obj->pollSelection);
            $out->element('poll:response', $data, '');
        }
    }

    /**
     * Called when generating JSON ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Modify the array contents to your heart's content, and it'll
     * all get serialized out as JSON.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param array &$out JSON-targeted array which can be modified
     */
    public function activityObjectOutputJson(ActivityObject $obj, array &$out)
    {
        common_log(LOG_DEBUG, 'QQQ: ' . var_export($obj, true));
        if (isset($obj->pollQuestion)) {
            /**
             * "poll": {
             *   "question": "Who wants a poll question?",
             *   "options": [
             *     "Option 1",
             *     "Option 2",
             *     "Option 3"
             *   ]
             * }
             */
            $data = array('question' => $obj->pollQuestion,
                'options' => array());
            foreach ($obj->pollOptions as $opt) {
                $data['options'][] = $opt;
            }
            $out['poll'] = $data;
        }
        if (isset($obj->pollSelection)) {
            /**
             * "pollResponse": {
             *   "poll": "http://..../poll/....",
             *   "selection": 3
             * }
             */
            $data = array('poll' => $obj->pollUri,
                'selection' => $obj->pollSelection);
            $out['pollResponse'] = $data;
        }
    }

    public function entryForm($out)
    {
        return new NewPollForm($out);
    }

    // @fixme is this from parent?
    public function tag()
    {
        return 'poll';
    }

    public function appTitle()
    {
        // TRANS: Application title.
        return _m('APPTITLE', 'Poll');
    }

    public function onStartAddNoticeReply($nli, $parent, $child)
    {
        // Filter out any poll responses
        if ($parent->object_type == self::POLL_OBJECT &&
            $child->object_type == self::POLL_RESPONSE_OBJECT) {
            return false;
        }
        return true;
    }

    // Hide poll responses for @chuck

    public function onEndNoticeWhoGets($notice, &$ni)
    {
        if ($notice->object_type == self::POLL_RESPONSE_OBJECT) {
            foreach ($ni as $id => $source) {
                $user = User::getKV('id', $id);
                if (!empty($user)) {
                    $pollPrefs = User_poll_prefs::getKV('user_id', $user->id);
                    if (!empty($pollPrefs) && ($pollPrefs->hide_responses)) {
                        unset($ni[$id]);
                    }
                }
            }
        }
        return true;
    }

    /**
     * Menu item for personal subscriptions/groups area
     *
     * @param Action $action action being executed
     *
     * @return boolean hook return
     */

    public function onEndAccountSettingsNav($action)
    {
        $action_name = $action->trimmed('action');

        $action->menuItem(
            common_local_url('pollsettings'),
            // TRANS: Poll plugin menu item on user settings page.
            _m('MENU', 'Polls'),
            // TRANS: Poll plugin tooltip for user settings menu item.
            _m('Configure poll behavior'),
            $action_name === 'pollsettings'
        );

        return true;
    }

    protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped = null)
    {
        if ($stored->object_type == self::POLL_RESPONSE_OBJECT) {
            parent::showNoticeContent($stored, $out, $scoped);
            return;
        }

        // If the stored notice is a POLL_OBJECT
        $poll = Poll::getByNotice($stored);
        if ($poll instanceof Poll) {
            if (!$scoped instanceof Profile || $poll->getResponse($scoped) instanceof Poll_response) {
                // Either the user is not logged in or it has already responded; show the results.
                $form = new PollResultForm($poll, $out);
            } else {
                $form = new PollResponseForm($poll, $out);
            }
            $form->show();
        } else {
            // TRANS: Error text displayed if no poll data could be found.
            $out->text(_m('Poll data is missing'));
        }
    }
}