#!/usr/bin/env python


import pathlib
import textwrap


FILE_PREFIX = '''\
# AUTOGENERATED WITH `make regen-grammar`
'''
NOT_PATH = r'\b(?<!\.|\.<|\.>)'
NOT_FQPATH = r'\b(?<!::|\.|\.<|\.>)'
# some unreserved keywords have special handling because they clash
# with builtins
EXCEPTION_KW = {'named', 'text', 'all'}


def pretty_join(sep, it, chunk=4, indent=12):
    out = []
    it = list(it)
    while it:
        out.append(sep.join(it[:chunk] + ['']).rstrip() + '\n')
        it = it[chunk:]

    # drop the last 3 characters ("| \n")
    out[-1] = out[-1][:-3]

    return textwrap.indent(''.join(out), ' ' * indent).lstrip()


def main():
    cur_path = pathlib.Path(__file__).resolve().parent

    with open(cur_path / 'meta') as f:
        # All this dance is needed because ST package repo refuses
        # to accept packages with *.py files in them (unless they
        # are actually an editor plugin.)
        meta_cnt = f.read()
        ns = {}
        exec(meta_cnt, globals(), ns)
        EdgeQL = ns['EdgeQL']

    eql_base_path = (
        cur_path.parent / 'grammars' / 'src'
    )

    dunder_builtins = [kw for kw in EdgeQL.reserved_keywords
                       if kw[:2] == '__']
    fn_builtins = sorted(EdgeQL.fn_builtins)
    index_builtins = sorted(EdgeQL.index_builtins)
    constraint_builtins = sorted(EdgeQL.constraint_builtins)
    type_builtins = sorted(set(EdgeQL.type_builtins) | {'anytype'})

    unreserved_keywords = sorted(
      set(EdgeQL.unreserved_keywords)
      # special keywords that have other meaning
      - set(dunder_builtins)
      - set(type_builtins)
      - set(constraint_builtins)
      - EXCEPTION_KW
    )
    reserved_keywords = sorted(
      set(EdgeQL.reserved_keywords)
      # special keywords that have other meaning
      - set(dunder_builtins)
      - set(type_builtins)
    )

    stdmodules = EdgeQL.module_builtins
    # Operators need to be sorted from longest to shortest to match
    # correctly. Lexicographical sort is added on top of that for
    # stability, but is not itself important.
    operators = sorted(EdgeQL.operators,
                       key=lambda x: (len(x), x), reverse=True)
    # the operator symbols need to be escaped
    operators = ['\\' + '\\'.join(op) for op in operators]

    # navigation punctuation needs to be processed similar to operators
    navigation = sorted(EdgeQL.navigation,
                        key=lambda x: (len(x), x), reverse=True)
    # the operator symbols need to be escaped
    navigation = ['\\' + '\\'.join(nav) for nav in navigation
                  # exclude '.' for the moment
                  if nav != '.']

    # operators
    with open(eql_base_path / 'operators.inc.syntax.yaml', 'wt') as f:
        f.write(textwrap.dedent(rf'''
        {FILE_PREFIX}
        ---
        match: |
          (?x)
            {pretty_join(' | ', operators, 8)}
        ...
        ''').strip())

    # operators
    with open(eql_base_path / 'keywords.inc.syntax.yaml', 'wt') as f:
        f.write(textwrap.dedent(rf'''
        {FILE_PREFIX}
        ---
        match: |
          (?ix) {NOT_FQPATH}(
            (?# special case)
            (named \s+ only)
            |
            (as \s+ text)
            |
            (all (?!\s*\())
            |

            (?# unreserved)
            {pretty_join(' | ', unreserved_keywords, 5, indent=12)}
            |
            (?# reserved)
            {pretty_join(' | ', reserved_keywords, 5, indent=12)}
          )\b
        ...
        ''').strip())

    # builtins
    with open(eql_base_path / 'builtins.inc.syntax.yaml', 'wt') as f:
        f.write(textwrap.dedent(rf'''
        {FILE_PREFIX}
        ---
        builtin-modules:
          name: support.other.module.builtin.edgeql
          match: |
            (?x) {NOT_FQPATH}(
              {pretty_join(' | ', stdmodules, indent=14)}
            )\b

        builtin-types:
          name: support.type.builtin.edgeql
          match: |
            (?x) {NOT_PATH} (
              {pretty_join(' | ', type_builtins, indent=14)}
            )\b

        builtin-indexes:
          name: support.other.index.builtin.edgeql
          match: |
            (?x) {NOT_PATH} (
              {pretty_join(' | ', index_builtins, indent=14)}
            )\b

        builtins:
          patterns:
            - name: support.function.builtin.edgeql
              match: |
                (?x) {NOT_PATH} (
                  {pretty_join(' | ', fn_builtins, 3, indent=18)}
                )(?=\s*\()\b

            - name: support.function.constraint.builtin.edgeql
              match: |
                (?x) {NOT_PATH} (
                  {pretty_join(' | ', constraint_builtins, 3, indent=18)}
                )\b

            - include: '#builtin-modules'

            - name: support.other.link.builtin.edgeql
              match: |
                (?x) \b(
                  {pretty_join(' | ', dunder_builtins, 3, indent=18)}
                )\b
        ...
        ''').strip())

    # function calls
    with open(eql_base_path / 'fncalls.inc.syntax.yaml', 'wt') as f:
        f.write(textwrap.dedent(rf'''
        {FILE_PREFIX}
        ---
        fncalls:
          patterns:
            - name: meta.function-call.edgeql
              begin: |
                (?x)
                  {NOT_PATH}
                  # function name
                  (?:
                    (
                      # functions
                      {pretty_join(' | ', fn_builtins, 3, indent=22)}
                    |
                      # constraints
                      {pretty_join(' | ', constraint_builtins, 3, indent=22)}
                    )
                    |
                    ([[:alpha:]_][[:alnum:]_]*)
                    |
                    (`.*?`)
                  ) \s*(\()
              end: (\))
              beginCaptures:
                '1': {{name: support.function.builtin.edgeql}}
                '2': {{name: entity.name.function.edgeql}}
                '3': {{name: string.interpolated.edgeql}}
                '4': {{name: punctuation.definition.arguments.begin.edgeql}}
              endCaptures:
                '1': {{name: punctuation.definition.arguments.end.edgeql}}
              patterns:
                - include: '#fncallargs'

            - name: meta.function-call.edgeql
              begin: |
                (?x)
                  {NOT_PATH}
                  # module
                  (?:
                    (
                      {pretty_join(' | ', stdmodules, 3, indent=22)}
                    )
                    |
                    (?# masking built-ins in odd ways)
                    (
                      #functions
                      {pretty_join(' | ', fn_builtins, 3, indent=22)}
                      |
                      #constraints
                      {pretty_join(' | ', constraint_builtins, 3, indent=22)}
                    )
                    |
                    ([[:alpha:]_][[:alnum:]_]*)
                    |
                    (`.*?`)
                  )

                  \s*(::)\s*

                  # function name
                  (?:
                    (
                      #functions
                      {pretty_join(' | ', fn_builtins, 3, indent=22)}
                      |
                      #constraints
                      {pretty_join(' | ', constraint_builtins, 3, indent=22)}
                    )
                    |
                    ([[:alpha:]_][[:alnum:]_]*)
                    |
                    (`.*?`)
                  ) \s*(\()
              end: (\))
              beginCaptures:
                '1': {{name: support.other.module.builtin.edgeql}}
                '2': {{name: support.function.builtin.edgeql}}
                '3': {{name: entity.name.function.edgeql}}
                '4': {{name: string.interpolated.edgeql}}
                '5': {{name: keyword.operator.namespace.edgeql}}
                '6': {{name: support.function.builtin.edgeql}}
                '7': {{name: entity.name.function.edgeql}}
                '8': {{name: string.interpolated.edgeql}}
                '9': {{name: punctuation.definition.arguments.begin.edgeql}}
              endCaptures:
                '1': {{name: punctuation.definition.arguments.end.edgeql}}
              patterns:
                - include: '#fncallargs'
        ...
        ''').strip())


if __name__ == '__main__':
    main()
