# Copyright (C) 2017 Johannes F. Knauf
#
# This program 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Use this rule to enforce alphabetical ordering of keys in mappings. The sorting
order uses the Unicode code point number as a default. As a result, the
ordering is case-sensitive and not accent-friendly (see examples below).
This can be changed by setting the global ``locale`` option.  This allows one
to sort case and accents properly.

.. rubric:: Options

* ``ignored-keys`` is a list of PCRE regexes to ignore some keys while checking
  order, if they match any regex.

.. rubric:: Default values (when enabled)

.. code-block:: yaml

 rules:
   key-ordering:
     ignored-keys: []

.. rubric:: Examples

#. With ``key-ordering: {}``

   the following code snippet would **PASS**:
   ::

    - key 1: v
      key 2: val
      key 3: value
    - {a: 1, b: 2, c: 3}
    - T-shirt: 1
      T-shirts: 2
      t-shirt: 3
      t-shirts: 4
    - hair: true
      hais: true
      haïr: true
      haïssable: true

   the following code snippet would **FAIL**:
   ::

    - key 2: v
      key 1: val

   the following code snippet would **FAIL**:
   ::

    - {b: 1, a: 2}

   the following code snippet would **FAIL**:
   ::

    - T-shirt: 1
      t-shirt: 2
      T-shirts: 3
      t-shirts: 4

   the following code snippet would **FAIL**:
   ::

    - haïr: true
      hais: true

#. With global option ``locale: "en_US.UTF-8"`` and rule ``key-ordering: {}``

   as opposed to before, the following code snippet would now **PASS**:
   ::

    - t-shirt: 1
      T-shirt: 2
      t-shirts: 3
      T-shirts: 4
    - hair: true
      haïr: true
      hais: true
      haïssable: true

#. With rule ``key-ordering: {ignored-keys: ["name"]}``

   the following code snippet would **PASS**:
   ::

    - a:
      b:
      name: ignored
      first-name: ignored
      c:
      d:
"""

from locale import strcoll
import re

import yaml

from yamllint.linter import LintProblem

ID = 'key-ordering'
TYPE = 'token'

CONF = {'ignored-keys': [str]}
DEFAULT = {'ignored-keys': []}
MAP, SEQ = range(2)


class Parent:
    def __init__(self, type):
        self.type = type
        self.keys = []


def check(conf, token, prev, next, nextnext, context):
    if 'stack' not in context:
        context['stack'] = []

    if isinstance(token, (yaml.BlockMappingStartToken,
                          yaml.FlowMappingStartToken)):
        context['stack'].append(Parent(MAP))
    elif isinstance(token, (yaml.BlockSequenceStartToken,
                            yaml.FlowSequenceStartToken)):
        context['stack'].append(Parent(SEQ))
    elif isinstance(token, (yaml.BlockEndToken,
                            yaml.FlowMappingEndToken,
                            yaml.FlowSequenceEndToken)):
        context['stack'].pop()
    elif (isinstance(token, yaml.KeyToken) and
          isinstance(next, yaml.ScalarToken)):
        # This check is done because KeyTokens can be found inside flow
        # sequences... strange, but allowed.
        if (len(context['stack']) > 0 and context['stack'][-1].type == MAP and
                not any(re.search(r, next.value)
                        for r in conf['ignored-keys'])):
            if any(strcoll(next.value, key) < 0
                   for key in context['stack'][-1].keys):
                yield LintProblem(
                    next.start_mark.line + 1, next.start_mark.column + 1,
                    f'wrong ordering of key "{next.value}" in mapping')
            else:
                context['stack'][-1].keys.append(next.value)
