<?php

/**
 * Plugin allow to set up short text messages with begin and expiration date which
 * will be shown in member area for some defined group of user.
 * @am_plugin_api 6.0
 * @setup_url admin-notification
 */
class Am_Plugin_Notification extends Am_Plugin
{
    const PLUGIN_STATUS = self::STATUS_PRODUCTION;
    const PLUGIN_COMM = self::COMM_COMMERCIAL;
    const PLUGIN_REVISION = '6.3.43';
    const ADMIN_PERM_ID = 'notification';
    const EVENT_NOTIFICATION_TEMPLATE_BEFORE_PARSE = 'notificationTemplateBeforeParse';

    static function getDbXml()
    {
        return <<<CUT
<schema version="4.0.0">
    <table name="notification">
        <field name="notification_id" type="int" notnull="1" extra="auto_increment"/>
        <field name="content" type="text"/>
        <field name="url" type="varchar" len="255"/>
        <field name="is_blank" type="tinyint" notnull="1"/>
        <field name="begin" type="date"/>
        <field name="expire" type="date"/>
        <field name="is_disabled" type="tinyint" notnull="1"/>
        <field name="limit" type="tinyint"/>
        <field name="is_custom" type="tinyint" notnull="1"/>
        <field name="css_class" type="varchar" len='32' notnull="1"/>
        <field name="sort_order" type="int"/>
        <field name="filter_products" type="text"/>

        <field name="countdown" type="tinyint" notnull="1"/>
        <field name="countdown_type" type="varchar" len="255"/>
        <field name="countdown_start" type="datetime" notnull="0" />
        <field name="countdown_format" type="varchar" len="255"/>
        <field name="countdown_period" type="int" notnull="1"/>
        <field name="countdown_products" type="varchar" len="255"/>

        <field name="zone" type="varchar" len="255"/>

        <index name="PRIMARY" unique="1">
            <field name="notification_id" />
        </index>
    </table>
    <table name="notification_access">
        <field name="notification_access_id" type="int" notnull="1" extra="auto_increment"/>
        <field name="notification_id" type="int"/>
        <field name="fn" type="enum" len="'product_id','product_category_id','free','user_id','user_group_id','free_without_login'"/>
        <field name="id" type="int"/>
        <field name="start_payments" type="int"/>
        <field name="start_days" type="int"/>
        <field name="stop_days" type="int"/>
        <field name="date_based" type="tinyint" notnull="1" default="0" />
        <index name="PRIMARY" unique="1">
            <field name="notification_access_id" />
        </index>
    </table>
    <table name="notification_click">
        <field name="notification_click_id" type="int" notnull="1" extra="auto_increment"/>
        <field name="notification_id" type="int"/>
        <field name="user_id" type="int" unsigned="1" notnull="0"/>
        <field name="dattm" type="datetime"/>
        <index name="PRIMARY" unique="1">
            <field name="notification_click_id" />
        </index>
    </table>
</schema>
CUT;
    }

    function init()
    {
        $this->getDi()->router->addRoute('notification', new Am_Mvc_Router_Route(
            'misc/notification/:action/:id', [
            'module' => 'default',
            'controller' => 'direct',
            'action' => 'index',
            'type' => 'misc',
            'plugin_id' => 'notification'
        ]));
    }

    function renderNotification()
    {
        $root = preg_replace('/https?:/', '', ROOT_URL);
        return <<<CUT
<div id="am-notifications"></div>
<script>
jQuery(document).ready(function() {
    jQuery.getScript('{$root}/misc/notification/js', function(){
        jQuery('#am-notifications').amNotifications({class: 'am-notification'});
    });
});
</script>

CUT;
    }

    function onAdminMenu(Am_Event $e)
    {
        $e->getMenu()->addPage([
            'id' => 'notification',
            'module' => 'default',
            'controller' => 'admin-notification',
            'action' => 'index',
            'label' => ___('Notifications'),
            'resource' => self::ADMIN_PERM_ID,
        ]);
    }

    function directAction($request, $response, $invokeArgs)
    {
        $user = $this->getDi()->auth->getUser();
        switch ($request->getActionName())
        {
            case 'c' :
                if ($id = $this->getDi()->security->reveal($request->getFiltered('id'))) { //actually it is notification_id
                    $notification = $this->getDi()->notificationTable->load($id);
                    $this->getDi()->notificationClickTable->log($user, $notification);
                    $tpl = new Am_SimpleTemplate();
                    $tpl->assign('user', $user);
                    return $response->redirectLocation($tpl->render($notification->url));
                }
                break;
            case 'get' :
                $items = $user ?
                    $this->getDi()->notificationTable->getNotificationsForUser($this->getDi()->auth->getUser()) :
                    $this->getDi()->notificationTable->getNotificationsForGuest();

                $dismiss = $this->getDismiss($user);

                $tpl = new Am_SimpleTemplate();
                $tpl->assign('user', $this->getDi()->auth->getUser());
                $this->getDi()->hook->call(
                    new Am_Event(self::EVENT_NOTIFICATION_TEMPLATE_BEFORE_PARSE,
                        ['template' => $tpl])
                    );

                $out = [];

                foreach ($items as $item) {
                    if ($request->getParam('zone', '') != $item->zone) {
                        continue;
                    }

                    $display = $this->getDisplayCount($user, $item);

                    if ($item->limit && $display >= $item->limit) {
                        continue;
                    }
                    if (empty($item->data()->get('dont_close')) && in_array($item->notification_id, $dismiss)) {
                        continue;
                    }

                    if ($item->countdown) {
                        if (!$item->countdown_period) continue;

                        $start = $this->getCountdownStart($user, $item);
                        if (!$start) continue;

                        $now = strtotime('now');
                        $begin = strtotime($start);
                        $expire = strtotime("$start +{$item->countdown_period}minutes");

                        if (($item->countdown_type == 'exact' && $now < $begin) || $now > $expire) {
                            continue;
                        }

                        $left = $expire - $now;
                        $format = $item->countdown_format ?: '{H}:{m}:{s}';

                        $tpl->assign('countdown', "<span class='am-notification-countdown' data-left='{$left}' data-format='{$format}'></span>");
                    }

                    $this->persistDisplayCount($user, $item, ++$display);

                    $n = new stdClass;
                    $n->id = $this->getDi()->app->obfuscate($item->notification_id);
                    $n->content = $tpl->render(___($item->content));
                    $n->is_custom = (bool)$item->is_custom;
                    $n->is_blank = (bool)$item->is_blank;
                    $n->link = $tpl->render($item->url);
                    $n->dont_close = $item->data()->get('dont_close');
                    $n->css_class = $item->css_class;
                    $out[] = $n;
                }
                $response->ajaxResponse($out);
                break;
            case 'js' :
                $response->setHeader('Content-Type', 'application/x-javascript; charset=utf-8');
                echo $this->getJs();
                break;
            case 'd' :
                $id = $this->getDi()->security->reveal($request->getFiltered('id'));
                if ($id) {
                    $this->dismiss($user, $id);
                }
                break;
            default :
                throw new Am_Exception_InternalError('Unknown Action');
        }
    }

    protected function dismiss(?User $user, int $id)
    {
        if ($user) {
            $_ = $user->data()->getBlob('notification.dismiss');
            $dismiss = $_ ? unserialize($_) : [];
            $dismiss[] = $id;
            $user->data()->setBlob('notification.dismiss', serialize($dismiss));
            $user->data()->update();
        } else {
            $cname = $this->getId() . '-d';
            $dismiss = !empty($_COOKIE[$cname]) ? explode(',', $_COOKIE[$cname]) : [];
            $dismiss[] = $id;

            Am_Cookie::set($cname, implode(',', $dismiss), strtotime('+1year'));
        }
    }

    protected function getDismiss(?User $user)
    {
        if ($user) {
            $_ = $user->data()->getBlob('notification.dismiss');
            $dismiss = $_ ? unserialize($_) : [];
        } else {
            $cname = $this->getId() . '-d';
            $dismiss = !empty($_COOKIE[$cname]) ? explode(',', $_COOKIE[$cname]) : [];
        }

        return $dismiss;
    }

    protected function getDisplayCount(?User $user, Notification $item): int
    {
        if ($user) {
            $cnt = $user->data()->get("notification.display.{$item->pk()}") ?: 0;
        } else {
            $cname = $this->getId() . "-{$item->pk()}-c";
            $cnt = $_COOKIE[$cname] ?? 0;
        }

        return $cnt;
    }

    protected function persistDisplayCount(?User $user, Notification $item, $display)
    {
        if ($user) {
            $user->data()->set("notification.display.{$item->pk()}", $display);
            $user->data()->update();
        } else {
            $cname = $this->getId() . "-{$item->pk()}-c";
            Am_Cookie::set($cname, $display, strtotime('+1year'));
        }
    }

    function getCountdownStart(?User $user, Notification $item)
    {
        switch ($item->countdown_type) {
            case 'access_begin':
                if (!$user) return null;
                $pids = $this->getDi()->productTable->extractProductIds($item->unserializeList($item->countdown_products));
                if (!$pids) return null;

                $_ = $this->getBegin($user->pk(), $pids);
                if (!$_) return null;

                return "{$_} 23:59:59";
            case 'added':
                if (!$user) return null;
                return $user->added;
            case 'exact':
                return $item->countdown_start;
        }
    }

    function getBegin($uid, $pids)
    {
        if (!$pids) return null;

        return $this->getDi()->db->selectCell(
            "SELECT MIN(begin_date) FROM ?_access WHERE user_id=?d AND product_id IN (?a)",
            $uid,
            $pids
        );
    }

    function getJs()
    {
        $root = preg_replace('/https?:/', '', ROOT_URL);
        $label = json_encode(___('Click here'));
        return <<<CUT
    (function($){
        \$.fn.amNotifications = function(params){
            return this.each(function(){
                let options = jQuery.extend({
                    class: null,
                    animation: 300,
                    link_text: $label,
                    zone: null,
                }, params);
                let \$this = jQuery(this);
                jQuery.getJSON("{$root}/misc/notification/get", {zone: options.zone}, function(data){
                    \$this.empty();
                    for (let i = 0; i < data.length; i++) {
                        let cont = jQuery('<div>');
                        let n = data[i];
                        cont.attr('id', 'am-notification-' + n.id);
                        if (options.class && !n.is_custom) {
                            cont.addClass(options.class);
                        } else if (!n.is_custom) {
                            cont.css({
                                'background-color': '#FFFFCF',
                                'border-color': '#545454',
                                'color': '#454430',
                                'border-radius': '10px',
                                'margin-bottom': '1em',
                                'padding': '0.5em 1em',
                                'border': '1px solid',
                            });
                        }
                        if (n.css_class) {
                            cont.addClass(n.css_class);
                        }
                        cont.append(n.content);
                        if (n.link) {
                            var l = jQuery("<a href='{$root}/misc/notification/c/"+n.id+"' " + (n.is_blank ? "target=_blank" : "") + " >" + options.link_text + "</a>");
                            cont.append("&nbsp;");
                            cont.append(l);
                        }
                        var link = jQuery("<span style='float:right;'><a href='#' style='text-decoration:none;' class='am-notification-close'>&#10005;</a></span>");
                        if (!n.dont_close) cont.prepend(link);
                        \$this.append(cont);
                        link.on('click', function(e){
                            e.preventDefault();
                            let id = jQuery(this).closest('div').attr('id').substr(16);
                            jQuery.get("{$root}/misc/notification/d/" + id, function(){
                                jQuery('#am-notification-'+id).hide(options.animation);
                                jQuery('#am-notification-'+id).remove();
                            });
                        });
                        cont.find('.am-notification-countdown').each(function(){
                            let left = jQuery(this).data('left');
                            let format = jQuery(this).data('format');
                            let current = cont;
                            let el = this;
                            let r = function() {
                                if (left <= 0) return;
                                let l = left;
                                let H = Math.floor(left / 3600)
                                let d = Math.floor(left / 3600 / 24)
                                l -= d * 3600 * 24;
                                let h = Math.floor(l / 3600)
                                l -= h * 3600;
                                let m = Math.floor(l / 60);
                                l -= m * 60
                                let s = l;
                                if (m < 10) m = "0" + m;
                                if (s < 10) s = "0" + s;
                                el.innerText = format.replace(/{s}/, s).replace(/{m}/, m).replace(/{h}/, h).replace(/{d}/, d).replace(/{H}/, H);
                                left--;
                                if (left <= 0) {
                                    current.remove();
                                }
                            };
                            r();
                            setInterval(r, 1000)
                        });
                    }
                });
            });
        }
    })(jQuery);
CUT;
    }

    function onGetPermissionsList(Am_Event $event)
    {
        $event->addReturn(___('Can Operate with Notifications'), self::ADMIN_PERM_ID);
    }

    function onInitBlocks(Am_Event $e)
    {
        $e->getBlocks()->add('member/main/top', new Am_Block_Base(null, 'notification', null, [$this, 'renderNotification']));
    }
}

class Am_Grid_Action_Sort_Notification extends Am_Grid_Action_Sort_Abstract
{
    protected function setSortBetween($item, $after, $before)
    {
        $this->_simpleSort($this->grid->getDi()->notificationTable, $item, $after, $before);
    }
}

class AdminNotificationController extends Am_Mvc_Controller_Grid
{
    function checkAdminPermissions(Admin $admin)
    {
        return $admin->hasPermission(Am_Plugin_Notification::ADMIN_PERM_ID);
    }

    function clickAction()
    {
        $ds = new Am_Query($this->getDi()->notificationClickTable);
        $ds->leftJoin('?_user', 'u', 't.user_id=u.user_id')
            ->addField('COUNT(t.notification_click_id)', 'cnt')
            ->addField('MAX(t.dattm)', 'click_date')
            ->addField('u.login')
            ->addField('u.email')
            ->addField("CONCAT(u.name_f, ' ', u.name_l)", 'name')
            ->groupBy('user_id', 'u')
            ->addWhere('notification_id=?', $this->getParam('id'));

        $grid = new Am_Grid_ReadOnly('_n_c', ___('Statistics'), $ds, $this->getRequest(), $this->getView(), $this->getDi());

        $userUrl = new Am_View_Helper_UserUrl();
        $grid->addField('login', ___('Username'))
            ->addDecorator(new Am_Grid_Field_Decorator_Link($userUrl->userUrl('{user_id}'), '_top'));
        $grid->addField('name', ___('Name'));
        $grid->addField('email', ___('E-Mail Address'));
        $grid->addField('cnt', ___('Clicks'), null, null, null, '1%');
        $grid->addField(new Am_Grid_Field_Date('click_date', ___('Last Click')));

        $grid->runWithLayout($this->layout);
    }

    function createGrid()
    {
        $ds = new Am_Query($this->getDi()->notificationTable);
        $ds->leftJoin('?_notification_click', 'nc', 't.notification_id=nc.notification_id')
            ->addField('COUNT(nc.notification_click_id)', 'cnt')
            ->addOrder('sort_order');

        $grid = new Am_Grid_Editable('_notification', ___('Notifications'), $ds, $this->_request, $this->view, $this->getDi());
        $grid->setPermissionId(Am_Plugin_Notification::ADMIN_PERM_ID);
        $grid->addField('css_class', ___('Type'))->setRenderFunction(function($record) use($grid){
            return $grid->renderTd($record->css_class ?:'warning');
        });
        $grid->addField('content', ___('Content'));
        $grid->addField(new Am_Grid_Field_Expandable('_access', ___('Target'), false))
            ->setPlaceholder([$this, 'getPlaceholder'])
            ->setRenderFunction([$this, 'renderProducts'])
            ->setGetFunction([$this, 'getProducts'])
            ->setMaxLength(200);

        if ($this->getDi()->notificationTable->getZones()) {
            $grid->addField('zone', ___('Zone'));
        }

        $grid->addField(new Am_Grid_Field_Date('begin', ___('Begin')))
            ->setFormatDate();
        $grid->addField(new Am_Grid_Field_Date('expire', ___('Expire')))
            ->setFormatDate();
        $grid->addField('cnt', ___('Clicks'), true, null, null, '1%')
            ->setRenderFunction([$this, 'renderClicks']);
        $grid->addField(new Am_Grid_Field_IsDisabled());
        $grid->setForm([$this, 'createForm']);
        $grid->addCallback(Am_Grid_Editable::CB_VALUES_TO_FORM, [$this, 'valuesToForm']);
        $grid->addCallback(Am_Grid_Editable::CB_VALUES_FROM_FORM, [$this, 'valuesFromForm']);

        $grid->setFormValueCallback('filter_products', ['RECORD', 'unserializeList'], ['RECORD', 'serializeList']);
        $grid->setFormValueCallback('countdown_products', ['RECORD', 'unserializeList'], ['RECORD', 'serializeList']);

        $grid->actionAdd(new Am_Grid_Action_Group_Delete());
        $grid->actionAdd(new Am_Grid_Action_Sort_Notification());
        $grid->actionAdd(new Am_Grid_Action_CopyNotification());
        $grid->addCallback(Am_Grid_ReadOnly::CB_RENDER_STATIC, [$this, 'renderStatic']);
        $grid->addCallback(Am_Grid_ReadOnly::CB_RENDER_CONTENT, [$this, 'renderContent']);

        $grid->setFilter(new Am_Grid_Filter_Text(___('Filter by Content or Link'), [
            'content' => 'LIKE',
            'url' => 'LIKE'
        ]));

        $grid->setRecordTitle(___('Notification'));
        return $grid;
    }

    public function renderProducts($obj, $fieldName, $controller, $field)
    {
        return $this->renderTd($field->get($obj, $controller), false);
    }

    public function getProducts(Am_Record $r)
    {
        $s = "";
        foreach ($this->getDi()->notificationAccessTable->findBy(['notification_id' => $r->pk()]) as $access) {
            $l = "";
            if ($access->getStart())
                $l .= " from " . $access->getStart();
            if ($access->getStop())
                $l .= " to " . $access->getStop();
            $s .= sprintf("%s <b>%s</b> %s<br />\n", $access->getClassTitle(), $access->getTitle(), $l);
        }
        return $s;
    }

    public function getPlaceholder($val, Am_Record $r)
    {
        return ___('%d access records&hellip;', $this->getDi()->notificationAccessTable->countBy(['notification_id' => $r->pk()]));
    }

    function renderContent(&$out)
    {
        $out .= <<<CUT
<div class="info" style="margin-top:1em"><pre>{$this->getReadme()}</pre></div>
CUT;
    }

    function getReadme()
    {
        $root = preg_replace('/https?:/', '', ROOT_URL);
        $script = <<<EOT
<div id="am-notifications"></div>
<script>
jQuery(document).ready(function() {
    jQuery.getScript('{$root}/misc/notification/js', function(){
        jQuery('#am-notifications').amNotifications();
    });
});
</script>
EOT;
        $script = Am_Html::escape($script);

        return <<<CUT
<p>Notifications can be placed outside aMember. In order to do this you should include this javascript on your page:</p>
<pre class="am-code">
{$script}
</pre>
<p>Above script uses jQuery library so make sure that jQuery is loaded in the header of your page.</p>
<p>In order to modify notifications look you can create CSS class which will be used instead of default styles:</p>
<pre class="am-code">
jQuery('#am-notifications').amNotifications({class: 'class-for-notification'});
</pre>
<p>You can use zone parameter to display notofication from specific zone:</p>
<pre class="am-code">
jQuery('#am-notifications').amNotifications({zone: 'zone-id'});
</pre>
CUT;
    }

    function renderStatic(&$out)
    {
        $title = json_encode(___('Statistics'));
        $out .= <<<CUT
<script>
jQuery(document).on('click', '.notification-click', function(){
    let div = jQuery('<div class="am-grid-wrap" id="grid-n_c"></div>');
    div.load(amUrl("/admin-notification/click/?id=") + jQuery(this).data('notification_id'), function(){
        div.dialog({
            autoOpen: true,
            width: 800,
            buttons: {},
            closeOnEscape: true,
            title: $title,
            modal: true,
            open: function(){
                div.ngrid();
            },
        })
    })
})
</script>
CUT;
    }

    function renderClicks($rec)
    {
        return $rec->cnt ?
            sprintf('<td style="text-align: center"><a href="javascript:;" class="local notification-click" data-notification_id="%d">%d</a></td>', $rec->pk(), $rec->cnt) :
            '<td></td>';
    }

    function valuesToForm(array & $values, Notification $record)
    {
        $values['_dont_close'] = $record->data()->get('dont_close');

        $is_custom = isset($values['is_custom']) && $values['is_custom'];
        @$values['_content_html'] = $is_custom ? $values['content'] : '';
        @$values['_content_text'] = $is_custom ? '' : $values['content'];
        foreach ((array) $record->_access as $access)
        {
            switch ($access->fn)
            {
                case 'free' :
                    $values['_target'][] = 'free';
                    break;
                case 'free_without_login' :
                    $values['_target'][] = 'free_without_login';
                    break;
                case 'user_id' :
                    $values['_target'][] = 'user_id';
                    $ids = [];
                    foreach ($record->_access as $r) {
                        if ($r->fn == 'user_id') {
                            $ids[] = $r->id;
                        }
                    }
                    $users = [];
                    foreach ($this->getDi()->userTable->loadIds($ids) as $user) {
                        $users[] = $user->login;
                    }

                    $values['_savedLoginOrEmail'] = implode(',', $users);
                    break;
                case 'user_group_id' :
                    $values['_target'][] = "user_group_id-{$access->id}";
                    break;
                case 'product_id':
                case 'product_category_id':
                    $values['_raccess'][] = $access;
                    break;
            }
        }
    }

    function valuesFromForm(array & $values, Notification $record)
    {
        $access = [];
        $record->data()->set('dont_close', !empty($values['_dont_close']));

        foreach ($values['_target'] as $v)
        {
            switch ($v)
            {
                case 'free' :
                    $rec = $this->getDi()->notificationAccessRecord;
                    $rec->fn = 'free';
                    $rec->id = null;
                    $access[] = $rec;
                    break;
                case 'free_without_login' :
                    $rec = $this->getDi()->notificationAccessRecord;
                    $rec->fn = 'free_without_login';
                    $rec->id = null;
                    $access[] = $rec;
                    break;
                case 'user_id' :
                    $user_ids = [];
                    foreach ($_REQUEST['_loginOrEmail'] as $loginOrEmail) {
                        $user = $this->getDi()->userTable->findFirstByLogin($loginOrEmail);
                        if (!$user)
                            $user = $this->getDi()->userTable->findFirstByEmail($loginOrEmail);

                        if ($user)
                            $user_ids[] = $user->pk();
                    }
                    $user_ids = array_filter($user_ids);
                    foreach ($user_ids as $uid) {
                        $rec = $this->getDi()->notificationAccessRecord;
                        $rec->fn = 'user_id';
                        $rec->id = $uid;
                        $access[] = $rec;
                    }
                    break;
                default:
                    preg_match('/(user_group_id)-([0-9]+)/i', $v, $match);
                    $rec = $this->getDi()->notificationAccessRecord;
                    $rec->fn = $match[1];
                    $rec->id = $match[2];
                    $access[] = $rec;
            }
        }
        foreach ([
            'product_id' => ResourceAccess::FN_PRODUCT,
            'product_category_id' => ResourceAccess::FN_CATEGORY
        ] as $key => $rtype) {

            if (!empty($values['_raccess'][$key])) {
                foreach ($values['_raccess'][$key] as $id => $params) {
                    if (!is_array($params))
                        $params = json_decode($params, true);

                    $fa = $this->getDi()->notificationAccessRecord;
                    $fa->fn = $rtype;
                    $fa->id = $id;
                    $fa->start_days = null;
                    $fa->start_payments = 0;
                    $fa->stop_days = null;
                    $fa->date_based = 0;
                    if (preg_match('/^(-?\d+)(\w+)$/', strtolower($params['start']), $regs))
                    {
                        switch ($regs[2]) {
                            case 'd':
                                $fa->start_days = $regs[1];
                                break;
                            case 'p':
                                $fa->start_payments = $regs[1];
                                break;
                        }
                    }
                    if (preg_match('/^(-?\d+)(\w+)$/', strtolower($params['stop']), $regs)) {
                        $fa->stop_days = $regs[1];
                    }
                    $access[] = $fa;
                }
            }
        }

        $values['expire'] = $values['expire'] ?: null;
        $values['begin'] = $values['begin'] ?: null;
        $values['url'] = $values['url'] ?: null;
        $values['limit'] = $values['limit'] ?: null;

        $record->_access = $access;
        $values['content'] = $values['is_custom'] ? $values['_content_html'] : $values['_content_text'];
    }

    function createForm()
    {
        $form = new Am_Form_Admin();

        $fs = $form->addFieldset()
            ->setLabel('Notification');

        $fs->addAdvRadio('is_custom')
            ->loadOptions([
                0 => ___('Use Pre-Defined Template'),
                1 => ___('Define Custom Html Message')
            ])->setValue(0);

        $fs->addTextarea('_content_text', ['rows' => '7', 'rel' => 'form-pre-defined', 'class' => 'am-row-wide am-el-wide translate'])
            ->setLabel(___("Content\n" .
                    'You can use all user specific placeholders here eg. %user.login%, %user.name_f%, %user.name_l% etc.'));

        $placeholder_items = &$options['placeholder_items'];
        foreach ($this->getUserTagOptions() as $k => $v) {
            $placeholder_items[] = [$v, $k];
        }
        $fs->addHtmlEditor('_content_html', ['rel' => 'form-html'], ['showInPopup' => true])
            ->setLabel(___("Content\n" .
                    'You can use all user specific placeholders here eg. %user.login%, %user.name_f%, %user.name_l% etc.'))
            ->setMceOptions($options);
        $grClass = $fs->addGroup()
            ->setLabel(___('Notification type'));
        $grClass->addSelect('css_class', ['id'=>'css-class-select'])->loadOptions([
            'warning' => 'warning',
            'info'  =>  'info',
            'success' => 'success',
            'danger' => 'danger'
        ]);
        $grClass->addStatic()->setContent("&nbsp;&nbsp;<div id='class-example' style='display:inline' class='am-el-wide am-notification warning'>Notification example</div>");

        $fs->addScript()
            ->setScript(<<<CUT
                jQuery("#css-class-select").on('change', function(){
                    let values = $.map($('#css-class-select option'),function(option) {
                        return option.value
                    })
                    jQuery('#class-example').removeClass(values.join(" ")).addClass(jQuery('#css-class-select').val())
                }).change()
CUT
            );

        $g = $fs->addGroup()
            ->setLabel(___('Link/URL'));
        $g->setSeparator('<br /><br />');

        $g->addText('url', ['class' => 'am-el-wide', 'rel' => 'form-pre-defined'])
            ->setLabel(___('Link'));

        $g->addAdvCheckbox('is_blank', ['rel' => 'form-pre-defined'], ['content' => ___('Open Link in New Window')]);

        $form->addScript()
            ->setScript(<<<CUT
jQuery('[name=is_custom]').change(function(){
    jQuery('[rel=form-pre-defined]').closest('.am-row').toggle(jQuery('[name=is_custom]:checked').val() == 0)
    jQuery('[rel=form-html]').closest('.am-row').toggle(jQuery('[name=is_custom]:checked').val() == 1)
    jQuery('#css-class-select').closest('.am-row').toggle(jQuery('[name=is_custom]:checked').val() == 0)
}).change()
CUT
        );

        $g = $form->addFieldset()
            ->setLabel('Conditions');

        $sel = $g->addMagicSelect('_target', ['class' => 'am-row-highlight'])
            ->setLabel(___('Target By User'));

        $cats = $pr = $gr = [];

        foreach ($this->getDi()->userGroupTable->getSelectOptions() as $k => $v)
            $gr['user_group_id-' . $k] = 'Group: ' . $v;

        $options = [
            'free' => ___('All registered customers'),
            'free_without_login' => ___('All visitors (without log-in and registration) and for all members'),
            'user_id' => ___('Specific User')
        ] + ($cats ? [___('Product Categories') => $cats] : []) + ($gr ? [___('User Groups') => $gr] : []) + ($pr ? [___('Products') => $pr] : []);

        $sel->loadOptions($options);
        //$sel->addRule('required');

        $sel->setJsOptions('{onChange:function(val){
            jQuery("input[name^=_loginOrEmail]").closest(\'.am-row\').toggle(val.hasOwnProperty("user_id"))
        }}');

        $loginGroup = $g->addGroup('', ['class' => 'am-row-highlight']);
        $loginGroup->addHtml()->setHtml('<div>' . ___('Username/Email') . '</div>');
        $loginGroup->addHidden('_savedLoginOrEmail')->setValue('');
        $loginGroup->addText('_loginOrEmail[]');

        $label_add_user = ___('Add User');
        $loginGroup->addHtml()
            ->setHtml(<<<CUT
<div><a href="javascript:void(null)" id="target-user_id-add" class="local">$label_add_user</a></div>
CUT
        );

        $g->addHtml(null, ['class' => 'am-row-highlight'])->setHtml("<strong>OR</strong>");

        $g->addElement(new Am_Form_Element_ResourceAccess('_raccess',
            ['without_free' => true, 'without_user_group_id' => true, 'class' => 'am-row-highlight']))
            ->setLabel('Target By Product');

        $pr = $cats = [];

        foreach ($this->getDi()->productCategoryTable->getOptions() as $k => $v)
            $cats['category-' . $k] = ___('Category') . ' : ' . $v;

        foreach ($this->getDi()->productTable->getOptions() as $k => $v)
            $pr['product-' . $k] = ___('Product') . ' : ' . $v;

        $fs = $form->addFieldset()
            ->setLabel('Limits');

        $sel = $fs->addMagicSelect('filter_products')->setLabel("Show notification only if user doesn't have");
        $sel->loadOptions([
            ___('Product Categories') => $cats,
            ___('Products') => $pr,
            ___('User Groups') => $gr
        ]);

        $gr = $fs->addGroup()
            ->setSeparator(' ')
            ->setLabel(___("Dates\n" .
                'date range when notification is shown'));

        $gr->addDate('begin', ['placeholder' => ___('Begin Date')]);
        $gr->addDate('expire', ['placeholder' => ___('Expire Date')]);

        $form->addScript('script')->setScript(<<<CUT
jQuery("input[name^=_loginOrEmail]").autocomplete({
    minLength: 2,
    source: amUrl("/admin-users/autocomplete"),
})
CUT
        );

        $delIcon = $this->getDi()->view->icon('delete');

        $form->addScript('script2')->setScript(<<<CUT
setTimeout(function(){
    let arr = jQuery('[name=_savedLoginOrEmail]').val().split(',').reverse()
    jQuery('[name^=_loginOrEmail]').val(arr.pop())
    for (let i in arr) {
        let \$field = addEmailOrLogin(jQuery('#target-user_id-add'));
        \$field.val(arr[i]);
    }
    
    function addEmailOrLogin(context) {
        let \$field = jQuery('<input type="text" name="_loginOrEmail[]" />')
        jQuery(context).before('<br />')
        jQuery(context).before(\$field)
        jQuery(context).before('<a href="javascript:void(null)" onclick="jQuery(this).prev().remove();jQuery(this).prev().remove();jQuery(this).next().remove();jQuery(this).remove()">$delIcon</a>')
        jQuery(context).before('<br />')
        \$field.autocomplete({
            minLength: 2,
            source: amUrl("/admin-users/autocomplete"),
        });
        return \$field
    }
    
    jQuery('#target-user_id-add').click(function(){
        addEmailOrLogin(this)
    })
});
CUT
        );

        $fs->addText('limit', ['size' => 8, 'placeholder' => ___('Unlimited')])
            ->setLabel(___("Limit Number of Display per User\n" .
                    'keep it empty for unlimited'));

        $fs = $form->addFieldset()
            ->setLabel(___('Miscellaneous'));

        $fs->addAdvcheckbox('_dont_close')
            ->setLabel(___('Does not allow to close notification'));

        $fs->addAdvCheckbox('countdown')
            ->setLabel("Enable Countdown\n"
                . "you can use placeholder %countdown% within notification if this options is enabled, "
                . "notification will be hidden as soon as countdown expire");

        $fs->addRaw()->setContent('<div id="countdown-options">');

        $fs->addText('countdown_format', ['placeholder' => "{H}:{m}:{s}", 'class' => 'am-el-wide'])
            ->setLabel("Countdown Format\n"
                . "{d} - days, {h} - hours, {m} - minutes, {s} - seconds, {H} - total number of hours");
        $fs->addText('countdown_period')
            ->setLabel("Countdown Period, minutes");

        $fs->addSelect('countdown_type')
            ->setLabel("Countdown Start")
            ->loadOptions([
               'added' => 'User Registration',
               'access_begin' => 'Access Begin Date',
               'exact' => 'Exact Date'
            ]);

        $fs->addDatetime('countdown_start')
            ->setLabel("Countdown Start Date and Time");

        $fs->addMagicSelect('countdown_products', ['class'=>'am-combobox-fixed'])
            ->setLabel("Countdown Products\n"
                . "to calculate start for countdown, earliest purchase date is used")
            ->loadOptions($this->getDi()->productTable->getProductOptions());

        $fs->addRaw()->setContent('</div>');

        $form->addScript()
            ->setScript(<<<CUT
jQuery(function(){
    jQuery('[name=countdown][type=checkbox]').change(function(){
        jQuery('#countdown-options').toggle(this.checked)
    }).change()

    jQuery('[name=countdown_type]').change(function(){
        jQuery('[name^=countdown_start]').closest('.am-row').toggle(jQuery(this).val() == 'exact')
        jQuery('[data-name^=countdown_products], [name^=countdown_products]').closest('.am-row').toggle(jQuery(this).val() == 'access_begin')
    }).change()
})
CUT
            );

        $rgroups = $this->getDi()->notificationTable->getZones();
        $roptions = ['' => ___('-- Please Select --')];
        if ($rgroups) {
            $roptions = $roptions + array_filter($rgroups);
        }

        $fs->addSelect('zone', ['id' => 'zone'], ['intrinsic_validation' => false, 'options' => $roptions])
            ->setLabel(___("Display Zone"));

        $form->addScript()
            ->setScript(<<<CUT
jQuery(function(){
    $("select[name='zone']").after($("<span> <a href='javascript:' id='add-zone' class='local'>add zone</a></span>"))
    $("select[name='zone']").change(function(){
      $(this).toggle($(this).find('option').length > 1)
    }).change()
    $("a#add-zone").click(function(){
      var ret = prompt("Enter title for your new zone, for example: zone#1", "")
      if (!ret) return
      var sel = $("select#zone").append($("<option></option>").val(ret).html(ret))
      sel.val(ret).change()
    })
})
CUT
);

        return $form;
    }

    function getUserTagOptions()
    {
        $tagOptions = [
            '%user.name_f%' => 'User First Name',
            '%user.name_l%' => 'User Last Name',
            '%user.login%' => 'Username',
            '%user.email%' => 'E-Mail',
            '%user.user_id%' => 'User Internal ID#',
            '%user.street%' => 'User Street',
            '%user.street2%' => 'User Street (Second Line)',
            '%user.city%' => 'User City',
            '%user.state%' => 'User State',
            '%user.zip%' => 'User ZIP',
            '%user.country%' => 'User Country',
            '%user.status%' => 'User Status (0-pending, 1-active, 2-expired)'
        ];

        foreach ($this->getDi()->userTable->customFields()->getAll() as $field) {
            if (@$field->sql && @$field->from_config) {
                $tagOptions['%user.' . $field->name . '%'] = 'User ' . $field->title;
            }
        }

        return $tagOptions;
    }
}

/**
 * {autogenerated}
 * @property int $notification_id PK
 * @property string|null $content
 * @property string|null $url
 * @property int $is_blank
 * @property date|null $begin
 * @property date|null $expire
 * @property int $is_disabled
 * @property int|null $limit
 * @property int $is_custom
 * @property string $css_class
 * @property int|null $sort_order
 * @property string|null $filter_products
 * @property int $countdown
 * @property string|null $countdown_type
 * @property datetime|null $countdown_start
 * @property string|null $countdown_format
 * @property int $countdown_period
 * @property string|null $countdown_products
 * @property string|null $zone
 * {/autogenerated}
 */
class Notification extends Am_Record_WithData
{
    function __get($name)
    {
        if (!$this->isLoaded())
            return null;
        if ($name == '_access') {
            $this->_access = $this->getDi()->notificationAccessTable->findBy(['notification_id' => $this->pk()]);
            return $this->_access;
        }
        throw new Am_Exception_InternalError('No variable ' . $name . ' defined in ' . __CLASS__);
    }

    function __isset($name)
    {
        return $name == '_access';
    }

    function update()
    {
        $ret = parent::update();

        $this->saveAccess($this->_access);
        return $ret;
    }

    function insert($reload = true)
    {
        $table_name = $this->getTable()->getName();
        $this->getAdapter()->query("UPDATE $table_name SET sort_order = sort_order + 1");

        $this->sort_order = 1;
        $ret = parent::insert($reload);
        $this->saveAccess($this->_access);

        return $ret;
    }

    function delete()
    {
        $ret = parent::delete();
        $this->getDi()->notificationAccessTable->deleteByNotificationId($this->pk(), false);
        $this->getDi()->notificationClickTable->deleteByNotificationId($this->pk(), false);

        $table_name = $this->getTable()->getName();
        $this->getAdapter()->query("UPDATE $table_name SET sort_order = sort_order - 1 WHERE sort_order > ?", $this->sort_order);
        return $ret;
    }

    protected function saveAccess($access)
    {
        if (!$this->isLoaded())
            return;

        $this->getDi()->notificationAccessTable->deleteBy(['notification_id' => $this->pk()]);

        foreach ($access as $v) {
            $v->notification_access_id = null;
            $v->notification_id = $this->pk();
            $v->insert();
        }
    }

    function isAvailableForUser(User $user)
    {
        // Check products;
        $filter_products = $this->filter_products ? explode(',', $this->filter_products) : [];

        $active_products = $user->getActiveProductIds();

        $categories = [];

        foreach ($this->getDi()->productCategoryTable->getCategoryProducts() as $cat_id => $products) {
            if (array_intersect($active_products, $products)) {
                $categories[] = $cat_id;
            }
        }
        $groups = $user->getGroups();

        foreach ($filter_products as $expr) {
            [$type, $id] = explode('-', $expr);

            switch ($type)
            {
                case 'category' :
                    if (in_array($id, $categories))
                        return false;
                    break;
                case 'product' :
                    if (in_array($id, $active_products))
                        return false;
                    break;
                case 'user_group_id' :
                    if (in_array($id, $groups))
                        return false;
                    break;
            }
        }

        return true;
    }
}

/** @extends Am_Table_WithData<Notification> */
class NotificationTable extends Am_Table_WithData
{
    protected $_table = '?_notification';
    protected $_recordClass = 'Notification';
    protected $_key = 'notification_id';

    function getNotificationsForUser(User $user)
    {
        return $this->getDi()->notificationAccessTable->getNotificationsForUser($user);
    }

    function getNotificationsForGuest()
    {
        return $this->getDi()->notificationAccessTable->getNotificationsForGuest();
    }

    function getZones()
    {
        return $this->_db->selectCol(
            "SELECT DISTINCT zone, zone AS ? FROM ?_notification WHERE zone <> '' ORDER BY zone",
            DBSIMPLE_ARRAY_KEY
        );
    }
}
/**
 * @extends ResourceAccessTable<NotificationAccess>
 * @method void deleteByNotificationId($value, $useObjects = true)
 */

class NotificationAccessTable extends ResourceAccessTable
{
    protected $_table = '?_notification_access';
    protected $_recordClass = 'NotificationAccess';
    protected $_key = 'notification_access_id';

    function getNotificationsByTargetUser(User $user)
    {
        $user_group_ids = $user->getGroups();
        array_push($user_group_ids, '-1'); //to avoide SQL error

        return $this->getDi()->db->selectCol("
            SELECT notification_id
            FROM ?_notification
            WHERE notification_id IN (
                SELECT notification_id
                FROM ?_notification_access
                WHERE
                    (fn=? AND id =  ?) OR
                    (fn=?) OR
                    (fn=?) OR
                    (fn=? AND id IN (?a))
            ) AND (begin IS NULL OR begin<=?)
              AND (expire IS NULL OR expire>=?)
              AND is_disabled=0
            ", 'user_id', $user->pk(), 'free', 'free_without_login', 'user_group_id', $user_group_ids, sqlDate('now'), sqlDate('now'));
    }

    function getNotificationsByTargetProduct(User $user)
    {
        return $this->getDi()->db->selectCol(<<<CUT
            SELECT notification_id
            FROM  ?_notification n
            WHERE n.notification_id IN (
                SELECT DISTINCT r.notification_id
                FROM ?_notification_access r
                LEFT JOIN ?_access_cache c
                ON
                    c.user_id=?
                    AND (
                        ((c.fn = r.fn) AND (c.id = r.id))
                        OR
                        (r.fn='product_category_id' AND r.id=-1)
                    )
                    AND (
                        (c.status='active' AND IFNULL(c.days,0) BETWEEN IFNULL(r.start_days,0) AND IFNULL(r.stop_days, 90000) AND c.payments_count >= IFNULL(r.start_payments,0))
                        OR
                        (c.days >= IFNULL(r.start_days,0) AND r.stop_days = -1 AND c.payments_count >= IFNULL(r.start_payments,0))
                    )
                WHERE c.user_id IS NOT NULL
            )
            AND  (n.begin IS NULL OR n.begin<=?)
            AND (n.expire IS NULL OR n.expire>=?)
            AND is_disabled=0
CUT
            , $user->pk(), sqlDate('now'), sqlDate('now'));
    }

    function getNotificationsForUser(User $user)
    {
        $ids = array_filter(
            array_unique(
                array_merge(
                    $a = $this->getNotificationsByTargetUser($user), $b = $this->getNotificationsByTargetProduct($user)
                )
            )
        );
        return $ids ?
            array_filter(
                $this->getDi()->notificationTable->selectObjects("SELECT * FROM ?_notification WHERE notification_id IN (?a) ORDER BY sort_order", $ids),
                function(Notification $notification) use ($user)
                {
                    return $notification->isAvailableForUser($user);
                }
                ) : [];
    }

    function getNotificationsForGuest()
    {
        return $this->getDi()->notificationTable->selectObjects("
            SELECT *
            FROM ?_notification
            WHERE notification_id IN (
                SELECT notification_id
                FROM ?_notification_access
                WHERE fn=?
            ) AND (begin IS NULL OR begin<=?)
              AND (expire IS NULL OR expire>=?)
              AND is_disabled=0
            ", 'free_without_login', sqlDate('now'), sqlDate('now'));
    }
}

/**
 * {autogenerated}
 * @property int $notification_access_id PK
 * @property int|null $notification_id FK notification
 * @property string|null $fn FK access_cache 'product_id'|'product_category_id'|'free'|'user_id'|'user_group_id'|'free_without_login'
 * @property int|null $id FK aff_bonus_link_product
 * @property int|null $start_payments
 * @property int|null $start_days
 * @property int|null $stop_days
 * @property int $date_based
 * {/autogenerated}
 */
class NotificationAccess extends ResourceAccess
{
    public function getTitle()
    {
        if ($this->fn == 'user_id') {
            $u = null;
            if ($this->id) {
                $u = $this->getDi()->userTable->load($this->id, false);
            }
            if (!$u) {
                return sprintf('(%s #%d)', $this->getClass(), $this->getId());
            }
            return sprintf('(%d) %s', $u->pk(), $u->login);
        } else {
            return parent::getTitle();
        }
    }

    public function getClassTitle()
    {
        switch ($this->fn)
        {
            case 'user_id' : return ___('User');
            default: return parent::getClassTitle();
        }
    }
}

class NotificationClick extends Am_Record
{
/** @extends Am_Table<NotificationClick> */
}

/**
 * @method void deleteByNotificationId($value, $useObjects = true)
 */
class NotificationClickTable extends Am_Table
{
    protected $_table = '?_notification_click';
    protected $_key = 'notification_click_id';
    protected $_recordClass = 'NotificationClick';

    function log(?Am_Record $user, Am_Record $notification)
    {
        $this->_db->query("INSERT INTO ?_notification_click SET ?a", [
            'user_id' => $user ? $user->pk() : null,
            'dattm' => sqlTime('now'),
            'notification_id' => $notification->pk()
        ]);
    }
}

class Am_Grid_Action_CopyNotification extends Am_Grid_Action_Abstract
{
    protected $id = 'copy';
    protected $privilege = 'insert';

    public function run()
    {
        $record = $this->grid->getRecord();

        $vars = $record->toRow();
        unset($vars['notification_id']);

        $back = @$_SERVER['HTTP_X_REQUESTED_WITH'];
        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest';
        $controller = new AdminNotificationController(
            new Am_Mvc_Request(),
            new Am_Mvc_Response(),
            ['di' => Am_Di::getInstance()]
        );

        $controller->valuesToForm($vars, $record);

        $vars['filter_products'] = $record->unserializeList($vars['filter_products']);
        $request = new Am_Mvc_Request($vars + [
                $this->grid->getId() . '_a' => 'insert',
                $this->grid->getId() . '_b' => $this->grid->getBackUrl()
            ], Am_Mvc_Request::METHOD_POST);

        $controller->setRequest($request);

        $request->setModuleName('default')
            ->setControllerName('admin-notification')
            ->setActionName('index')
            ->setDispatched(true);

        $controller->dispatch('indexAction');
        $response = $controller->getResponse();
        $response->sendResponse();
        $_SERVER['HTTP_X_REQUESTED_WITH'] = $back;
    }
}
