<?php
/*********************************************************************
    class.queue.php

    Custom (ticket) queues for osTicket

    Jared Hancock <jared@osticket.com>
    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR . 'class.search.php';

class CustomQueue extends SavedSearch {
    static $meta = array(
        'joins' => array(
            'columns' => array(
                'reverse' => 'QueueColumn.queue',
            ),
        ),
    );

    static function objects() {
        return parent::objects()->filter(array(
            'flags__hasbit' => static::FLAG_QUEUE
        ));
    }

    static function getDecorations($root) {
        // Ticket decorations
        return array(
            'TicketThreadCount',
            'ThreadAttachmentCount',
            'OverdueFlagDecoration',
            'TicketSourceDecoration'
        );
    }

    function getColumns() {
        if (!count($this->columns)) {
            foreach (array(
                new QueueColumn(array(
                    "id" => 1,
                    "heading" => "Number",
                    "primary" => 'number',
                    "width" => 100,
                )),
                new QueueColumn(array(
                    "id" => 2,
                    "heading" => "Created",
                    "primary" => 'created',
                    "width" => 100,
                )),
                new QueueColumn(array(
                    "id" => 3,
                    "heading" => "Subject",
                    "primary" => 'cdata__subject',
                    "width" => 250,
                )),
                new QueueColumn(array(
                    "id" => 4,
                    "heading" => "From",
                    "primary" => 'user__name',
                    "width" => 150,
                )),
                new QueueColumn(array(
                    "id" => 5,
                    "heading" => "Priority",
                    "primary" => 'cdata__priority',
                    "width" => 120,
                )),
                new QueueColumn(array(
                    "id" => 6,
                    "heading" => "Assignee",
                    "primary" => 'assignee',
                    "secondary" => 'team__name',
                    "width" => 100,
                )),
            ) as $c) {
                $this->addColumn($c);
            }
        }
        return $this->columns;
    }

    function addColumn(QueueColumn $col) {
        $this->columns->add($col);
        $col->queue = $this;
    }

    function getRoot() {
        return 'Ticket';
    }

    function getBasicQuery($form=false) {
        $root = $this->getRoot();
        $query = $root::objects();
        return $this->mangleQuerySet($query, $form);
    }

    /**
     * Retrieve a QuerySet instance based on the type of object (root) of
     * this Q, which is automatically configured with the data and criteria
     * of the queue and its columns.
     *
     * Returns:
     * <QuerySet> instance
     */
    function getQuery($form=false) {
        $query = $this->getBasicQuery($form);
        foreach ($this->getColumns() as $C) {
            $query = $C->mangleQuery($query);
        }
        return $query;
    }
}

abstract class QueueDecoration {
    static $icon = false;
    static $desc = '';

    var $config;

    function __construct($config) {
        $this->config = $config;
    }

    static function fromJson($config) {
        $class = $config['c'];
        if (class_exists($class))
            return new $class($config);
    }

    static function getDescription() {
        return __(static::$desc);
    }
    static function getIcon() {
        return static::$icon;
    }
    static function getPositions() {
        return array(
            "<" => __('Start'),
            "b" => __('Before'),
            "a" => __('After'),
            ">" => __('End'),
        );
    }

    function decorate($text, $dec) {
        static $positions = array(
            '<' => '<span class="pull-left">%2$s</span>%1$s',
            '>' => '<span class="pull-right">%2$s</span>%1$s',
            'a' => '%1$s %2$s',
            'b' => '%2$s %1$s',
        );

        $pos = $this->getPosition();
        if (!isset($positions[$pos]))
            return $text;

        return sprintf($positions[$pos], $text, $dec);
    }

    // Render the annotation with the database record $row. $text is the
    // text of the cell before decorations were applied.
    function render($row, $cell) {
        if ($decoration = $this->getDecoration($row, $cell))
            return $this->decorate($cell, $decoration);

        return $cell;
    }

    // Add the annotation to a QuerySet
    abstract function annotate($query);

    // Fetch some HTML to render the decoration on the page. This function
    // can return boolean FALSE to indicate no decoration should be applied
    abstract function getDecoration($row, $text);

    function getPosition() {
        return strtolower($this->config['p']) ?: 'a';
    }
}

class TicketThreadCount
extends QueueDecoration {
    static $icon = 'comments-alt';
    static $qname = '_thread_count';
    static $desc = /* @trans */ 'Thread Count';

    function annotate($query) {
        return $query->annotate(array(
        static::$qname => TicketThread::objects()
            ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
            ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
            ->aggregate(array('count' => SqlAggregate::COUNT('entries__id')))
        ));
    }

    function getDecoration($row, $text) {
        $threadcount = $row[static::$qname];
        if ($threadcount > 1) {
            return sprintf(
                '<small class="faded-more"><i class="icon-comments-alt"></i> %s</small>',
                $threadcount
            );
        }
    }
}

class ThreadAttachmentCount
extends QueueDecoration {
    static $icon = 'paperclip';
    static $qname = '_att_count';
    static $desc = /* @trans */ 'Attachment Count';

    function annotate($query) {
        // TODO: Convert to Thread attachments
        return $query->annotate(array(
        static::$qname => TicketThread::objects()
            ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
            ->filter(array('entries__attachments__inline' => 0))
            ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id')))
        ));
    }

    function getDecoration($row, $text) {
        $count = $row[static::$qname];
        if ($count) {
            return sprintf(
                '<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="%s"></i>',
                $count);
        }
    }
}

class OverdueFlagDecoration
extends QueueDecoration {
    static $icon = 'exclamation';
    static $desc = /* @trans */ 'Overdue Icon';

    function annotate($query) {
        return $query->values('isoverdue');
    }

    function getDecoration($row, $text) {
        if ($row['isoverdue'])
            return '<span class="Icon overdueTicket"></span>';
    }
}

class TicketSourceDecoration
extends QueueDecoration {
    static $icon = 'phone';
    static $desc = /* @trans */ 'Ticket Source';

    function annotate($query) {
        return $query->values('source');
    }

    function getDecoration($row, $text) {
        return sprintf('<span class="Icon %sTicket"></span>',
            strtolower($row['source']));
    }
}

class DataSourceField
extends ChoiceField {
    function getChoices() {
        $config = $this->getConfiguration();
        $root = $config['root'];
        $fields = array();
        foreach (SavedSearch::getSearchableFields($root) as $path=>$f) {
            $fields[$path] = $f->get('label');
        }
        return $fields;
    }
}

class QueueColumnCondition {
    var $config;
    var $properties = array();

    function __construct($config) {
        $this->config = $config;
        if (is_array($config['prop']))
            $this->properties = $config['prop'];
    }

    function getProperties() {
        return $this->properties;
    }

    function getField() {
    }

    // Add the annotation to a QuerySet
    function annotate($query) {
        $criteria = $this->config['crit'];
        $searchable = SavedSearch::getSearchableFields('Ticket');

        // Setup a dummy form with a source for field setup
        $form = new Form($criteria);
        $fields = array();
        foreach ($criteria as $k=>$v) {
            if (substr($k, -7) === '+method') {
                list($name,) = explode('+', $k, 2);
                if (!isset($searchable[$name]))
                    continue;

                // Lookup the field to search this condition
                $field = $searchable[$name];

                // Get the search method and value
                $breakout = SavedSearch::getSearchField($field, $name);
                $method = $breakout["{$name}+method"];
                $method->setForm($form);
                if (!($method = $method->getClean()))
                    continue;

                if (!($value = $breakout["{$name}+{$method}"]))
                    continue;

                // Fetch a criteria Q for the query
                $value = $value->getClean();
                $Q = $field->getSearchQ($method, $value, $name);

                // Add an annotation to the query
                $query = $query->annotate(array(
                    $this->getAnnotationName() => new SqlExpr($Q)
                ));

                // Only one field can be considered in the condition
                break;
            }
        }
        return $query;
    }

    function render($row, $text) {
        $field = $this->getAnnotationName();
        if ($V = $row[$field]) {
            $style = array();
            foreach ($this->getProperties() as $css=>$value) {
                $style[] = "{$css}:{$value}";
            }
            $text = sprintf('<span style="%s">%s</span>',
                implode(' ', $style), $text);
        }
        return $text;
    }

    function getAnnotationName() {
        return 'howdy';
    }

    static function fromJson($config) {
        if (is_string($config))
            $config = JsonDataParser::decode($cnofig);
        if (!is_array($config))
            throw new BadMethodCallException('$config must be string or array');

        return new static($config);
    }
}

class QueueColumnConditionProperty
extends ChoiceField {
    static $properties = array(
        'background-color' => 'ColorChoiceField',
        'color' => 'ColorChoiceField',
        'font-family' => array(
            'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
        ),
        'font-size' => array(
            'small', 'medium', 'large', 'smaller', 'larger',
        ),
        'font-style' => array(
            'normal', 'italic', 'oblique',
        ),
        'font-weight' => array(
            'lighter', 'normal', 'bold', 'bolder',
        ),
        'text-decoration' => array(
            'none', 'underline',
        ),
        'text-transform' => array(
            'uppercase', 'lowercase', 'captalize',
        ),
    );

    function __construct($property) {
        $this->property = $property;
    }

    static function getProperties() {
        return array_keys(static::$properties[$this->property]);
    }

    static function getField($prop) {
        $choices = static::$properties[$prop];
        if (is_array($choices))
            return new ChoiceField(array(
                'choices' => array_combine($choices, $choices),
            ));
        elseif (class_exists($choices))
            return new $choices();
    }

    function getChoices() {
        if (isset($this->property))
            return static::$properties[$this->property];

        $keys = array_keys(static::$properties);
        return array_combine($keys, $keys);
    }
}


/**
 * Object version of JSON-serialized column array which has several
 * properties:
 *
 * {
 *   "heading": "Header Text",
 *   "primary": "user__name",
 *   "secondary": null,
 *   "width": 100,
 *   "link": 'ticket',
 *   "truncate": "wrap",
 *   "filter": "UsersName"
 *   "annotations": [
 *     {
 *       "c": "ThreadCollabCount",
 *       "p": ">"
 *     }
 *   ],
 *   "conditions": [
 *     {
 *       "crit": {
 *         "created+method": {"ndaysago": "in the last n days"}, "created+ndaysago": {"until":"7"}
 *       },
 *       "prop": {
 *         "font-weight": "bold"
 *       }
 *     }
 *   ]
 * }
 */
class QueueColumn
extends VerySimpleModel {
    static $meta = array(
        'table' => QUEUE_COLUMN_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'queue' => array(
                'constraint' => array('queue_id' => 'CustomQueue.id'),
            ),
        ),
    );

    var $_decorations = array();
    var $_conditions = array();

    function __onload() {
        if ($this->annotations) {
            foreach ($this->annotations as $D)
                $this->_decorations[] = QueueDecoration::fromJson($D) ?: array();
        }
        if ($this->conditions) {
            foreach ($this->conditions as $C)
                $this->_conditions[] = QueueColumnCondition::fromJson($C) ?: array();
        }
    }

    function getId() {
        return $this->id;
    }

    function getQueue() {
        return $this->queue;
    }

    function getHeading() {
        return $this->heading;
    }

    function getWidth() {
        return $this->width ?: 100;
    }

    function getLink($row) {
        $link = $this->link;
        switch (strtolower($link)) {
        case 'root':
        case 'ticket':
            return Ticket::getLink($row['ticket_id']);
        case 'user':
            return User::getLink($row['user_id']);
        case 'org':
            return Organization::getLink($row['user__org_id']);
        }
    }

    function render($row) {
        // Basic data
        $text = $this->renderBasicValue($row);

        // Truncate
        if ($class = $this->getTruncateClass()) {
            $text = sprintf('<span class="%s">%s</span>', $class, $text);
        }

        // Link
        if ($link = $this->getLink($row)) {
            $text = sprintf('<a href="%s">%s</a>', $link, $text);
        }

        // Decorations and conditions
        foreach ($this->_decorations as $D) {
            $text = $D->render($row, $text);
        }
        foreach ($this->_conditions as $C) {
            $text = $C->render($row, $text);
        }
        return $text;
    }

    function renderBasicValue($row) {
        $root = $this->getQueue()->getRoot();
        $fields = SavedSearch::getSearchableFields($root);
        $primary = $this->getOrmPath($this->primary);
        $secondary = $this->getOrmPath($this->secondary);

        // TODO: Consider data filter if configured

        if (($F = $fields[$primary]) && ($T = $F->from_query($row, $primary)))
            return $F->display($F->to_php($T));

        if (($F = $fields[$secondary]) && ($T = $F->from_query($row, $secondary)))
            return $F->display($F->to_php($T));
    }

    function getTruncateClass() {
        switch ($this->truncate) {
        case 'ellipsis':
            return 'trucate';
        case 'clip':
            return 'truncate clip';
        default:
        case 'wrap':
            return false;
        }
    }

    function mangleQuery($query) {
        // Basic data
        $fields = SavedSearch::getSearchableFields($this->getQueue()->getRoot());
        if ($primary = $fields[$this->primary])
            $query = $primary->addToQuery($query,
                $this->getOrmPath($this->primary));

        if ($secondary = $fields[$this->secondary])
            $query = $secondary->addToQuery($query,
                $this->getOrmPath($this->secondary));

        switch ($this->link) {
        // XXX: Consider the ROOT of the related queue
        case 'ticket':
            $query = $query->values('ticket_id');
            break;
        case 'user':
            $query = $query->values('user_id');
            break;
        case 'org':
            $query = $query->values('user__org_id');
            break;
        }

        // Decorations
        foreach ($this->_decorations as $D) {
            $query = $D->annotate($query);
        }

        // Conditions
        foreach ($this->_conditions as $C) {
            $query = $C->annotate($query);
        }

        return $query;
    }

    function getDataConfigForm($source=false) {
        return new QueueColDataConfigForm($source ?: $this->ht,
            array('id' => $this->id));
    }

    function getOrmPath($name) {
        return $name;
    }

    function getDecorations() {
        return $this->_decorations;
    }

    function getConditions() {
        return $this->_conditions;
    }

    /**
     * Create a CustomQueueColumn from vars (_POST) received from an
     * update request.
     */
    static function create($vars=array()) {
        $inst = parent::create($vars);
        // TODO: Convert decorations and conditions
        return $inst;
    }

    function update($vars) {
        $form = $this->getDataConfigForm($vars);
        foreach ($form->getClean() as $k=>$v)
            $this->set($k, $v);

        // Do the decorations
        $this->_decorations = $this->decorations = array();
        foreach ($vars['decorations'] as $i=>$class) {
            if (!class_exists($class) || !is_subclass_of($class, 'QueueDecoration'))
                continue;
            if ($vars['deco_column'][$i] != $this->id)
                continue;
            $json = array('c' => $class, 'p' => $vars['deco_pos'][$i]);
            $this->_decorations[] = QueueDecoration::fromJson($json);
            $this->decorations[] = $json;
        }
        // Store as JSON array
        $this->decorations = JsonDataEncoder::encode($this->decorations);
    }
}

class QueueColDataConfigForm
extends AbstractForm {
    function buildFields() {
        return array(
            'primary' => new DataSourceField(array(
                'label' => __('Primary Data Source'),
                'required' => true,
                'configuration' => array(
                    'root' => 'Ticket',
                ),
                'layout' => new GridFluidCell(6),
            )),
            'secondary' => new DataSourceField(array(
                'label' => __('Secondary Data Source'),
                'configuration' => array(
                    'root' => 'Ticket',
                ),
                'layout' => new GridFluidCell(6),
            )),
            'heading' => new TextboxField(array(
                'label' => __('Heading'),
                'required' => true,
                'layout' => new GridFluidCell(3),
            )),
            'link' => new ChoiceField(array(
                'label' => __('Link'),
                'required' => false,
                'choices' => array(
                    'ticket' => __('Ticket'),
                    'user' => __('User'),
                    'org' => __('Organization'),
                ),
                'layout' => new GridFluidCell(3),
            )),
            'width' => new TextboxField(array(
                'label' => __('Width'),
                'default' => 75,
                'configuration' => array(
                    'validator' => 'number',
                ),
                'layout' => new GridFluidCell(3),
            )),
            'truncate' => new ChoiceField(array(
                'label' => __('Text Overflow'),
                'choices' => array(
                    'wrap' => __("Wrap Lines"),
                    'ellipsis' => __("Add Ellipsis"),
                    'clip' => __("Clip Text"),
                ),
                'default' => 'wrap',
                'layout' => new GridFluidCell(3),
            )),
        );
    }
}