<?php
/**
 * SegmentPlugin for phplist.
 *
 * This file is a part of SegmentPlugin.
 *
 * SegmentPlugin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * SegmentPlugin 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 General Public License for more details.
 *
 * @category  phplist
 *
 * @author    Duncan Cameron
 * @copyright 2014-2017 Duncan Cameron
 * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License, Version 3
 */
use phpList\plugin\Common\Container;
use phpList\plugin\Common\ImageTag;
use phpList\plugin\Common\PageLink;
use phpList\plugin\Common\PageURL;
use phpList\plugin\SegmentPlugin\Segment;

class SegmentPlugin extends phplistPlugin
{
    const VERSION_FILE = 'version.txt';
    const GUIDANCE = 'https://resources.phplist.com/plugin/segment#add_segment_conditions';

    /*
     *  Inherited variables
     */
    public $name = 'Segmentation';
    public $authors = 'Duncan Cameron';
    public $description = 'Send to a subset of subscribers using custom conditions';
    public $documentationUrl = 'https://resources.phplist.com/plugin/segment';
    public $settings;

    private $error_level;
    private $selectedSubscribers = array();
    private $dao;
    private $conditionFactory;
    private $logger;
    private $segment = null;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->coderoot = dirname(__FILE__) . '/' . __CLASS__ . '/';
        $this->settings = array(
            'segment_campaign_max' => array(
                'description' => s('The maximum number of earlier campaigns to select from'),
                'type' => 'integer',
                'value' => 10,
                'allowempty' => 0,
                'min' => 4,
                'max' => 25,
                'category' => 'Segmentation',
            ),
            'segment_subscribers_max' => array(
                'description' => s('The maximum number of selected subscribers to display'),
                'type' => 'integer',
                'value' => 50,
                'allowempty' => 0,
                'min' => 5,
                'max' => 500,
                'category' => 'Segmentation',
            ),
            'segment_saved_summary' => array(
                'description' => s('Summary of saved segments'),
                'type' => 'textarea',
                'value' => '',
                'allowempty' => true,
                'category' => 'Segmentation',
            ),
            'segment_attribute_max_length' => array(
                'description' => s('Limit the display length of an attribute name. Enter 0 to always display the full attribute name'),
                'type' => 'integer',
                'value' => 0,
                'allowempty' => true,
                'min' => 0,
                'max' => 100,
                'category' => 'Segmentation',
            ),
        );
        $this->error_level = E_ALL ^ E_NOTICE ^ E_DEPRECATED ^ E_STRICT;

        parent::__construct();
        $this->version = (is_file($f = $this->coderoot . self::VERSION_FILE))
            ? file_get_contents($f)
            : '';
    }

    /**
     * Provide the dependencies for enabling this plugin.
     *
     * @return array
     */
    public function dependencyCheck()
    {
        global $plugins;

        return array(
            'phpList version 3.3.2 or later' => version_compare(VERSION, '3.3.2') >= 0,
            'Common plugin version 3.11.0 or greater must be enabled' => (
                phpListPlugin::isEnabled('CommonPlugin')
                && version_compare($plugins['CommonPlugin']->version, '3.11.0') >= 0
            ),
            'PHP version 5.4.0 or greater' => version_compare(PHP_VERSION, '5.4') > 0,
        );
    }

    public function activate()
    {
        parent::activate();

        $depends = require $this->coderoot . 'depends.php';
        $container = new Container($depends);
        $this->dao = $container->get('SegmentPlugin_DAO');
        $this->conditionFactory = $container->get('ConditionFactory');
        $this->logger = $container->get('Logger');
    }

    /**
     * List of items to add to menu.
     *
     * @return array
     */
    public function adminmenu()
    {
        return array();
    }

    /**
     * The title of the additional tab.
     *
     * @param int $messageId
     *
     * @return string the title
     */
    public function sendMessageTabTitle($messageid = 0)
    {
        return s('Segment');
    }

    /**
     * Build contents of additional tab.
     *
     * @param int   $messageId
     * @param array $messageData
     *
     * @return string the html for the tab
     */
    public function sendMessageTab($messageId = 0, $messageData = array())
    {
        global $pagefooter;

        $er = error_reporting($this->error_level);

        $formFields = isset($messageData['segment']) ? $messageData['segment'] : array();
        $segment = $this->createSegment($messageId, $formFields);
        $this->processSegmentCommands($segment, $formFields);

        $selectPrompt = s('Select ...');
        $params = array();
        $params['condition'] = array();
        $params['selectPrompt'] = $selectPrompt;

        foreach ($segment->conditions() as $i => $c) {
            $s = new stdClass();
            $params['condition'][] = $s;
            $field = $c['field'];

            try {
                $type = $this->conditionFactory->createConditionType($field, $messageData);
            } catch (SegmentPlugin_ConditionException $e) {
                $s->error = s('Unable to create condition - %s', $e->getMessage());
                continue;
            }

            // display field selection drop-down list
            $s->fieldList = $this->fieldDropDownList($field, $i, $selectPrompt);

            // display hidden input to detect when field changes
            $s->hiddenField = CHtml::hiddenField("segment[c][$i][_field]", $field);

            // display operators drop-down list
            $operators = $type->operators();
            $selected = isset($c['op']) ? $c['op'] : key($operators);
            $s->operatorList = CHtml::dropDownList(
                "segment[c][$i][op]",
                $selected,
                $operators,
                array('class' => 'autosubmit')
            );

            // display value
            $value = isset($c['value']) ? $c['value'] : '';
            $s->display = $type->display($selected, $value, "segment[c][$i]");
        }

        // add empty field
        $s = new stdClass();
        $i = count($segment->conditions());
        $s->fieldList = $this->fieldDropDownList('', $i, $selectPrompt);
        $s->hiddenField = CHtml::hiddenField("segment[c][$i][_field]", '');
        $params['condition'][] = $s;

        // display warning if no lists have been selected
        if (!(is_array($messageData['targetlist']) && count($messageData['targetlist']) > 0)) {
            $params['warning'] = s('Please select at least one list on the Lists tab before adding segmentation conditions');
        }

        // display fields for saved segments only where there are some
        $saved = new SegmentPlugin_SavedSegments();
        $savedListData = $saved->selectListData();

        if (count($savedListData) > 0) {
            $params['savedList'] = CHtml::dropDownList(
                'segment[usesaved][]',
                '',
                $savedListData,
                array('multiple' => 'multiple', 'style' => 'width: 50%')
            );
            $params['loadButton'] = CHtml::submitButton(s('Load segments'), array('name' => 'segment[load]'));
            $params['settingsButton'] = new PageLink(
                new PageURL('configure', array(), 'segmentation'),
                'Edit saved segments',
                array('target' => '_blank', 'class' => 'button')
            );
        }

        // display combine drop-down list
        $params['combineList'] = CHtml::dropDownList(
            'segment[combine]',
            $segment->combine(),
            array(SegmentPlugin_Operator::ONE => s('any'), SegmentPlugin_Operator::ALL => s('all'))
        );

        // display calculate button
        $params['calculateButton'] = CHtml::submitButton(s('Calculate'), array('name' => 'segment[calculate]'));

        // display calculated number of subscribers
        if (isset($formFields['calculate'])) {
            try {
                list($params['totalSubscribers'], $params['subscribers']) = $segment->calculateSubscribers(getConfig('segment_subscribers_max'));
                $params['exportCalculatedButton'] = new PageLink(
                    new PageURL('export', array('pi' => 'SegmentPlugin', 'id' => $messageId)),
                    'Export subscribers',
                    array('class' => 'button dialog')
                );
            } catch (SegmentPlugin_ValueException $e) {
                $params['warning'] = s('Invalid value for segment condition');
            } catch (SegmentPlugin_ConditionException $e) {
                $params['warning'] = s('Unable to create condition - %s', $e->getMessage());
            }
        }

        // display remove all, save button and input field only when there is at least one entered condition
        if (count($segment->conditions()) > 0) {
            $params['removeButton'] = CHtml::submitButton(s('Remove all'), array('name' => 'segment[remove]'));
            $params['saveButton'] = CHtml::submitButton(s('Save segment'), array('name' => 'segment[save]'));
            $params['saveName'] = CHtml::textField('segment[savename]', '', array('size' => 25, 'placeholder' => 'Name of segment'));
        }

        // display link to Help page
        $params['help'] = CHtml::tag(
            'a',
            array('href' => self::GUIDANCE, 'target' => '_blank'),
            new ImageTag('info.png', 'Guidance')
        );
        $html = $this->render('sendtab.tpl.php', $params);
        $pagefooter[basename(__FILE__)] = file_get_contents($this->coderoot . 'script.html');
        error_reporting($er);

        return $html;
    }

    /**
     * Validate that conditions are valid for the message to be submitted by calculating the number of subscribers.
     *
     * @param array $messageData
     *
     * @return string empty string for allow, otherwise error text to be displayed
     */
    public function allowMessageToBeQueued($messageData = array())
    {
        if (!isset($messageData['segment']['c'])) {
            return '';
        }
        $segment = $this->createSegment($messageData['id'], $messageData['segment']);

        try {
            $segment->validateSegment();
        } catch (SegmentPlugin_ValueException $e) {
            return s('Invalid value for segment condition');
        } catch (SegmentPlugin_ConditionException $e) {
            return s('Unable to create condition - %s', $e->getMessage());
        }

        return '';
    }

    /**
     * Use this hook to delete the 'not sent' rows from the usermessage table
     * so that they will be re-evaluated.
     *
     * @param int $id the message id
     *
     * @return none
     */
    public function messageQueued($id)
    {
        $this->dao->deleteNotSent($id);
    }

    /**
     * The same processing as when queueing a message.
     *
     * @param int $id the message id
     *
     * @return none
     */
    public function messageReQueued($id)
    {
        $this->messageQueued($id);
    }

    /**
     * Use this hook to select the subscribers who meet the segment conditions.
     * $selectedSubscribers will contain the selected subscribers.
     *
     * @param array $messageData the message data
     *
     * @return none
     */
    public function campaignStarted($messageData = array())
    {
        $er = error_reporting($this->error_level);
        $messageId = $messageData['id'];
        $this->selectedSubscribers[$messageId] = null;

        if (isset($messageData['segment']['c'])) {
            $segment = $this->createSegment($messageId, $messageData['segment']);

            try {
                list($total, $this->selectedSubscribers[$messageId]) = $segment->loadSubscribers();
                logEvent(s('Segment plugin selected %d subscribers for campaign %d', $total, $messageId));
            } catch (SegmentPlugin_ValueException $e) {
                logEvent(s('Invalid value for segment condition'));
            } catch (SegmentPlugin_ConditionException $e) {
                logEvent(s('Unable to create condition - %s', $e->getMessage()));
            } catch (SegmentPlugin_NoConditionsException $ex) {
                // do nothing
            }
        }
        error_reporting($er);
    }

    /**
     * Determine whether the campaign should be sent to a specific user.
     *
     * @param array $messageData the message data
     * @param array $userData    the user data
     *
     * @return bool
     */
    public function canSend($messageData, $userData)
    {
        $messageId = $messageData['id'];
        $userId = (int) $userData['id'];

        return ($this->selectedSubscribers[$messageId] === null)
            ? true
            : (bool) $this->selectedSubscribers[$messageId][$userId];
    }

    /**
     * Build the html to be added to the view message page.
     *
     * @param int   $messageId   the message id
     * @param array $messageData the message data
     *
     * @return array|false the caption and html to be added, or false if the message
     *                     does not use segments
     */
    public function viewMessage($messageId, array $messageData)
    {
        if (!isset($messageData['segment'])) {
            return false;
        }
        $formFields = $messageData['segment'];

        if (!isset($formFields['c'])) {
            return false;
        }
        $segment = $this->createSegment($messageId, $formFields);
        $conditions = $segment->conditions();

        if (count($conditions) == 0) {
            return false;
        }
        $params = array();
        $params['condition'] = array();

        foreach ($conditions as $i => $c) {
            $s = new stdClass();
            $params['condition'][] = $s;
            $field = $c['field'];

            try {
                $type = $this->conditionFactory->createConditionType($field, $messageData);
            } catch (SegmentPlugin_ConditionException $e) {
                $s->error = sprintf('Unable to create condition - %s', $e->getMessage());
                continue;
            }

            // display field selection
            $fields = $this->conditionFactory->subscriberFields() + $this->conditionFactory->attributeFields();
            $s->field = $fields[$field];

            // display operator
            $operators = $type->operators();
            $op = isset($c['op']) ? $c['op'] : key($operators);
            $s->operator = $operators[$op];

            // display value field
            $value = isset($c['value']) ? $c['value'] : '';
            $s->display = $type->display($op, $value, "segment[c][$i]");
        }

        // display combine
        $combineOps = array(SegmentPlugin_Operator::ONE => s('any'), SegmentPlugin_Operator::ALL => s('all'));
        $params['combine'] = $combineOps[$segment->combine()];
        list($params['totalSubscribers']) = $segment->calculateSubscribers(1);
        $html = $this->render('viewmessage.tpl.php', $params);

        return array('Segment conditions', $html);
    }

    /**
     * Called when a campaign is being copied.
     * Allows this plugin to specify which rows of the messagedata table should also
     * be copied.
     *
     * @return array rows of messagedata table that should be copied
     */
    public function copyCampaignHook()
    {
        return array('segment');
    }

    private function render($template, $params)
    {
        extract($params);
        ob_start();
        require $this->coderoot . $template;

        return ob_get_clean();
    }

    private function fieldDropDownList($field, $seq, $selectPrompt)
    {
        return CHtml::dropDownList(
            "segment[c][$seq][field]",
            $field,
            array('Subscriber Data' => $this->conditionFactory->subscriberFields(), 'Attributes' => $this->conditionFactory->attributeFields()),
            array('prompt' => $selectPrompt, 'class' => 'autosubmit searchable')
        );
    }

    /**
     * Process segment commands that can change the set of conditions.
     *
     * @param Segment $segment
     * @param array   $formFields
     */
    private function processSegmentCommands($segment, $formFields)
    {
        $saved = new SegmentPlugin_SavedSegments();

        if (isset($formFields['remove'])) {
            /*
             *  Remove all conditions
             */
            $segment->removeAll();
        } elseif (isset($formFields['save']) && $formFields['savename'] != '') {
            /*
             *  Save the current set of conditions
             */
            $saved->addSegment($formFields['savename'], $segment->conditions());
        } elseif (isset($formFields['load']) && isset($formFields['usesaved']) && is_array($formFields['usesaved'])) {
            /*
             *  Load the selected saved segments
             */
            try {
                foreach ($formFields['usesaved'] as $savedId) {
                    $savedConditions = $saved->segmentById($savedId);
                    $segment->addConditions($savedConditions);
                }
            } catch (Exception $e) {
                // do nothing
            }
        }
    }

    /**
     * Create a segment from the message form fields.
     *
     * @param int   $messageId
     * @param array $formFields
     *
     * @return Segment
     */
    private function createSegment($messageId, $formFields)
    {
        return new Segment(
            $messageId,
            isset($formFields['c']) ? $formFields['c'] : array(),
            isset($formFields['combine']) ? $formFields['combine'] : SegmentPlugin_Operator::ALL,
            $this->conditionFactory
        );
    }
}
