summaryrefslogtreecommitdiffstats
path: root/xbmc/interfaces/info/InfoExpression.cpp
blob: dc9aa63e1e505b88a06086192001af5ebb85a507 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
 *  Copyright (C) 2005-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "InfoExpression.h"

#include "GUIInfoManager.h"
#include "ServiceBroker.h"
#include "guilib/GUIComponent.h"
#include "utils/log.h"

#include <list>
#include <memory>
#include <stack>

using namespace INFO;

void InfoSingle::Initialize()
{
  m_condition = CServiceBroker::GetGUI()->GetInfoManager().TranslateSingleString(m_expression, m_listItemDependent);
}

void InfoSingle::Update(int contextWindow, const CGUIListItem* item)
{
  // use propagated context in case this info has the default context (i.e. if not tied to a specific window)
  // its value might depend on the context in which the evaluation was called
  int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context;
  m_value = CServiceBroker::GetGUI()->GetInfoManager().GetBool(m_condition, context, item);
}

void InfoExpression::Initialize()
{
  if (!Parse(m_expression))
  {
    CLog::Log(LOGERROR, "Error parsing boolean expression {}", m_expression);
    m_expression_tree = std::make_shared<InfoLeaf>(CServiceBroker::GetGUI()->GetInfoManager().Register("false", 0), false);
  }
}

void InfoExpression::Update(int contextWindow, const CGUIListItem* item)
{
  // use propagated context in case this info expression has the default context (i.e. if not tied to a specific window)
  // its value might depend on the context in which the evaluation was called
  int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context;
  m_value = m_expression_tree->Evaluate(context, item);
}

/* Expressions are rewritten at parse time into a form which favours the
 * formation of groups of associative nodes. These groups are then reordered at
 * evaluation time such that nodes whose value renders the evaluation of the
 * remainder of the group unnecessary tend to be evaluated first (these are
 * true nodes for OR subexpressions, or false nodes for AND subexpressions).
 * The end effect is to minimise the number of leaf nodes that need to be
 * evaluated in order to determine the value of the expression. The runtime
 * adaptability has the advantage of not being customised for any particular skin.
 *
 * The modifications to the expression at parse time fall into two groups:
 * 1) Moving logical NOTs so that they are only applied to leaf nodes.
 *    For example, rewriting ![A+B]|C as !A|!B|C allows reordering such that
 *    any of the three leaves can be evaluated first.
 * 2) Combining adjacent AND or OR operations such that each path from the root
 *    to a leaf encounters a strictly alternating pattern of AND and OR
 *    operations. So [A|B]|[C|D+[[E|F]|G] becomes A|B|C|[D+[E|F|G]].
 */

bool InfoExpression::InfoLeaf::Evaluate(int contextWindow, const CGUIListItem* item)
{
  return m_invert ^ m_info->Get(contextWindow, item);
}

InfoExpression::InfoAssociativeGroup::InfoAssociativeGroup(
    node_type_t type,
    const InfoSubexpressionPtr &left,
    const InfoSubexpressionPtr &right)
    : m_type(type)
{
  AddChild(right);
  AddChild(left);
}

void InfoExpression::InfoAssociativeGroup::AddChild(const InfoSubexpressionPtr &child)
{
  m_children.push_front(child); // largely undoes the effect of parsing right-associative
}

void InfoExpression::InfoAssociativeGroup::Merge(const std::shared_ptr<InfoAssociativeGroup>& other)
{
  m_children.splice(m_children.end(), other->m_children);
}

bool InfoExpression::InfoAssociativeGroup::Evaluate(int contextWindow, const CGUIListItem* item)
{
  /* Handle either AND or OR by using the relation
   * A AND B == !(!A OR !B)
   * to convert ANDs into ORs
   */
  std::list<InfoSubexpressionPtr>::iterator last = m_children.end();
  std::list<InfoSubexpressionPtr>::iterator it = m_children.begin();
  bool use_and = (m_type == NODE_AND);
  bool result = use_and ^ (*it)->Evaluate(contextWindow, item);
  while (!result && ++it != last)
  {
    result = use_and ^ (*it)->Evaluate(contextWindow, item);
    if (result)
    {
      /* Move this child to the head of the list so we evaluate faster next time */
      m_children.push_front(*it);
      m_children.erase(it);
    }
  }
  return use_and ^ result;
}

/* Expressions are parsed using the shunting-yard algorithm. Binary operators
 * (AND/OR) are treated as right-associative so that we don't need to make a
 * special case for the unary NOT operator. This has no effect upon the answers
 * generated, though the initial sequence of evaluation of leaves may be
 * different from what you might expect.
 */

InfoExpression::operator_t InfoExpression::GetOperator(char ch)
{
  if (ch == '[')
    return OPERATOR_LB;
  else if (ch == ']')
    return OPERATOR_RB;
  else if (ch == '!')
    return OPERATOR_NOT;
  else if (ch == '+')
    return OPERATOR_AND;
  else if (ch == '|')
    return OPERATOR_OR;
  else
    return OPERATOR_NONE;
}

void InfoExpression::OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes)
{
  operator_t op2 = operator_stack.top();
  operator_stack.pop();
  if (op2 == OPERATOR_NOT)
  {
    invert = !invert;
  }
  else
  {
    // At this point, it can only be OPERATOR_AND or OPERATOR_OR
    if (invert)
      op2 = (operator_t) (OPERATOR_AND ^ OPERATOR_OR ^ op2);
    node_type_t new_type = op2 == OPERATOR_AND ? NODE_AND : NODE_OR;

    InfoSubexpressionPtr right = nodes.top();
    nodes.pop();
    InfoSubexpressionPtr left = nodes.top();

    node_type_t right_type = right->Type();
    node_type_t left_type = left->Type();

    // Combine associative operations into the same node where possible
    if (left_type == new_type && right_type == new_type)
      /* For example:        AND
       *                   /     \                ____ AND ____
       *                AND       AND     ->     /    /   \    \
       *               /   \     /   \         leaf leaf leaf leaf
       *             leaf leaf leaf leaf
       */
      std::static_pointer_cast<InfoAssociativeGroup>(left)->Merge(std::static_pointer_cast<InfoAssociativeGroup>(right));
    else if (left_type == new_type)
      /* For example:        AND                    AND
       *                   /     \                /  |  \
       *                AND       OR      ->   leaf leaf OR
       *               /   \     /   \                  /   \
       *             leaf leaf leaf leaf              leaf leaf
       */
      std::static_pointer_cast<InfoAssociativeGroup>(left)->AddChild(right);
    else
    {
      nodes.pop();
      if (right_type == new_type)
      {
        /* For example:        AND                       AND
         *                   /     \                   /  |  \
         *                OR        AND     ->      OR  leaf leaf
         *               /   \     /   \           /   \
         *             leaf leaf leaf leaf       leaf leaf
         */
        std::static_pointer_cast<InfoAssociativeGroup>(right)->AddChild(left);
        nodes.push(right);
      }
      else
        /* For example:        AND              which can't be simplified, and
         *                   /     \            requires a new AND node to be
         *                OR        OR          created with the two OR nodes
         *               /   \     /   \        as children
         *             leaf leaf leaf leaf
         */
        nodes.push(std::make_shared<InfoAssociativeGroup>(new_type, left, right));
    }
  }
}

bool InfoExpression::Parse(const std::string &expression)
{
  const char *s = expression.c_str();
  std::string operand;
  std::stack<operator_t> operator_stack;
  bool invert = false;
  std::stack<InfoSubexpressionPtr> nodes;
  // The next two are for syntax-checking purposes
  bool after_binaryoperator = true;
  int bracket_count = 0;

  CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();

  char c;
  // Skip leading whitespace - don't want it to count as an operand if that's all there is
  while (isspace((unsigned char)(c=*s)))
    s++;

  while ((c = *s++) != '\0')
  {
    operator_t op;
    if ((op = GetOperator(c)) != OPERATOR_NONE)
    {
      // Character is an operator
      if ((!after_binaryoperator && (c == '!' || c == '[')) ||
          (after_binaryoperator && (c == ']' || c == '+' || c == '|')))
      {
        CLog::Log(LOGERROR, "Misplaced {}", c);
        return false;
      }
      if (c == '[')
        bracket_count++;
      else if (c == ']' && bracket_count-- == 0)
      {
        CLog::Log(LOGERROR, "Unmatched ]");
        return false;
      }
      if (!operand.empty())
      {
        InfoPtr info = infoMgr.Register(operand, m_context);
        if (!info)
        {
          CLog::Log(LOGERROR, "Bad operand '{}'", operand);
          return false;
        }
        /* Propagate any listItem dependency from the operand to the expression */
        m_listItemDependent |= info->ListItemDependent();
        nodes.push(std::make_shared<InfoLeaf>(info, invert));
        /* Reuse operand string for next operand */
        operand.clear();
      }

      // Handle any higher-priority stacked operators, except when the new operator is left-bracket.
      // For a right-bracket, this will stop with the matching left-bracket at the top of the operator stack.
      if (op != OPERATOR_LB)
      {
        while (!operator_stack.empty() && operator_stack.top() > op)
          OperatorPop(operator_stack, invert, nodes);
      }
      if (op == OPERATOR_RB)
        operator_stack.pop(); // remove the matching left-bracket
      else
        operator_stack.push(op);
      if (op == OPERATOR_NOT)
        invert = !invert;

      if (c == '+' || c == '|')
        after_binaryoperator = true;
      // Skip trailing whitespace - don't want it to count as an operand if that's all there is
      while (isspace((unsigned char)(c=*s))) s++;
    }
    else
    {
      // Character is part of operand
      operand += c;
      after_binaryoperator = false;
    }
  }
  if (bracket_count > 0)
  {
    CLog::Log(LOGERROR, "Unmatched [");
    return false;
  }
  if (after_binaryoperator)
  {
    CLog::Log(LOGERROR, "Missing operand");
    return false;
  }
  if (!operand.empty())
  {
    InfoPtr info = infoMgr.Register(operand, m_context);
    if (!info)
    {
      CLog::Log(LOGERROR, "Bad operand '{}'", operand);
      return false;
    }
    /* Propagate any listItem dependency from the operand to the expression */
    m_listItemDependent |= info->ListItemDependent();
    nodes.push(std::make_shared<InfoLeaf>(info, invert));
  }
  while (!operator_stack.empty())
    OperatorPop(operator_stack, invert, nodes);

  m_expression_tree = nodes.top();
  return true;
}