// Copied from https://github.com/microsoft/monaco-languages/blob/d7cc098c481059f63d51ce3753975c8ca8ab6030/src/python/python.ts

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import type * as monaco from 'monaco-editor';

/** The Pybricks MicroPython language identifier. */
export const pybricksMicroPythonId = 'pybricks-micropython';

export const conf: monaco.languages.LanguageConfiguration = {
    comments: {
        lineComment: '#',
        blockComment: ["'''", "'''"],
    },
    brackets: [
        ['{', '}'],
        ['[', ']'],
        ['(', ')'],
    ],
    autoClosingPairs: [
        { open: '{', close: '}' },
        { open: '[', close: ']' },
        { open: '(', close: ')' },
        { open: '"', close: '"', notIn: ['string'] },
        { open: "'", close: "'", notIn: ['string', 'comment'] },
    ],
    surroundingPairs: [
        { open: '{', close: '}' },
        { open: '[', close: ']' },
        { open: '(', close: ')' },
        { open: '"', close: '"' },
        { open: "'", close: "'" },
    ],
    onEnterRules: [
        {
            beforeText: new RegExp(
                '^\\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$',
            ),
            action: { indentAction: <monaco.languages.IndentAction.Indent>1 },
        },
    ],
    folding: {
        offSide: true,
        markers: {
            start: new RegExp('^\\s*#region\\b'),
            end: new RegExp('^\\s*#endregion\\b'),
        },
    },
};

export const language = <monaco.languages.IMonarchLanguage>{
    defaultToken: '',
    tokenPostfix: '.python',

    // https://docs.python.org/3/reference/lexical_analysis.html#keywords
    keywords: <ReadonlyArray<string>>[
        'False',
        'None',
        'True',
        'and',
        'as',
        'assert',
        'async',
        'await',
        'break',
        'class',
        'continue',
        'def',
        'del',
        'elif',
        'else',
        'except',
        'finally',
        'for',
        'from',
        'global',
        'if',
        'import',
        'in',
        'is',
        'lambda',
        'nonlocal',
        'not',
        'or',
        'pass',
        'raise',
        'return',
        'try',
        'while',
        'with',
        'yield',
    ],

    // https://docs.python.org/3/library/functions.html#built-in-funcs
    builtins: <ReadonlyArray<string>>[
        'abs',
        'all',
        'any',
        'ascii',
        'bin',
        'bool',
        'breakpoint',
        'bytearray',
        'bytes',
        'callable',
        'chr',
        'classmethod',
        'compile',
        'complex',
        'delattr',
        'dict',
        'dir',
        'divmod',
        'enumerate',
        'eval',
        'exec',
        'filter',
        'float',
        'format',
        'frozenset',
        'getattr',
        'globals',
        'hasattr',
        'hash',
        'help',
        'hex',
        'id',
        'input',
        'int',
        'isinstance',
        'issubclass',
        'iter',
        'len',
        'list',
        'locals',
        'map',
        'max',
        'memoryview',
        'min',
        'next',
        'object',
        'oct',
        'open',
        'ord',
        'pow',
        'print',
        'property',
        'reversed',
        'range',
        'repr',
        'reversed',
        'round',
        'self',
        'set',
        'setattr',
        'slice',
        'sorted',
        'staticmethod',
        'str',
        'sum',
        'super',
        'tuple',
        'type',
        'vars',
        'zip',
        '__import__',
    ],

    brackets: [
        { open: '{', close: '}', token: 'delimiter.curly' },
        { open: '[', close: ']', token: 'delimiter.bracket' },
        { open: '(', close: ')', token: 'delimiter.parenthesis' },
    ],

    tokenizer: {
        root: [
            { include: '@whitespace' },
            { include: '@numbers' },
            { include: '@strings' },
            [/@[a-zA-Z_]\w*/, 'tag'],
            [/\.\.\./, 'keyword'],
            [/->/, 'delimiter'],
            { include: '@operators' },
            [/[,:.;=]/, 'delimiter'],
            [/[{}[\]()]/, '@brackets'],
            [
                /[a-zA-Z_]\w*/,
                {
                    cases: {
                        '@keywords': 'keyword',
                        '@builtins': 'support.function',
                        '[A-Z_]+': 'support.constant',
                        '@default': 'identifier',
                    },
                },
            ],
        ],

        // Deal with white space, including single and multi-line comments
        whitespace: [
            [/\s+/, 'white'],
            [/(^#.*$)/, 'comment'],
            [/'''/, 'string', '@endDocString'],
            [/"""/, 'string', '@endDblDocString'],
        ],
        endDocString: [
            [/[^']+/, 'string'],
            [/\\'/, 'string'],
            [/'''/, 'string', '@popall'],
            [/'/, 'string'],
        ],
        endDblDocString: [
            [/[^"]+/, 'string'],
            [/\\"/, 'string'],
            [/"""/, 'string', '@popall'],
            [/"/, 'string'],
        ],

        // Recognize binary, octal, hex, decimals, imaginary, and scientific notation
        numbers: [
            [/\b0[bB](0|1|_)+/, 'constant.numeric.bin'],
            [/\b0[oO]([0-7]|_)+/, 'constant.numeric.oct'],
            [/\b0[xX]([abcdef]|[ABCDEF]|\d|_)+/, 'constant.numeric.hex'],
            [/\b(\d[\d_]*\.|\.)?\d[\d_]*([eE][+-]?[\d_]+)?[jJ]?/, 'constant.numeric'],
        ],

        // Recognize strings, including those broken across lines with \ (but not without)
        strings: [
            [/'$/, 'string.escape', '@popall'],
            [/([fF]|[fF][rR]|[rR][fF])?'/, 'string.escape', '@fStringBody'],
            [/[rRuUbB]?'/, 'string.escape', '@stringBody'],
            [/"$/, 'string.escape', '@popall'],
            [/([fF]|[fF][rR]|[rR][fF])?"/, 'string.escape', '@dblFStringBody'],
            [/[rRuUbB]?"/, 'string.escape', '@dblStringBody'],
        ],
        fStringBody: [
            [/([^\\'{}]|\{\{|\}\}(?!\}[^}]))+$/, 'string', '@popall'],
            [/([^\\'{}]|\{\{|\}\}(?!\}[^}]))+/, 'string'],
            [/\\./, 'string'],
            [/'/, 'string.escape', '@popall'],
            [/\\$/, 'string'],
            [
                /\{/,
                {
                    token: 'delimiter.curly',
                    next: '@fStringReplacement',
                    nextEmbedded: pybricksMicroPythonId,
                },
            ],
            [/\}/, 'delimiter.curly'],
        ],
        dblFStringBody: [
            [/([^\\"{}]|\{\{|\}\}(?!\}[^}]))+$/, 'string', '@popall'],
            [/([^\\"{}]|\{\{|\}\}(?!\}[^}]))+/, 'string'],
            [/\\./, 'string'],
            [/"/, 'string.escape', '@popall'],
            [/\\$/, 'string'],
            [
                /\{/,
                {
                    token: 'delimiter.curly',
                    next: '@fStringReplacement',
                    nextEmbedded: pybricksMicroPythonId,
                },
            ],
            [/\}/, 'delimiter.curly'],
        ],
        stringBody: [
            [/[^\\']+$/, 'string', '@popall'],
            [/[^\\']+/, 'string'],
            [/\\./, 'string'],
            [/'/, 'string.escape', '@popall'],
            [/\\$/, 'string'],
        ],
        dblStringBody: [
            [/[^\\"]+$/, 'string', '@popall'],
            [/[^\\"]+/, 'string'],
            [/\\./, 'string'],
            [/"/, 'string.escape', '@popall'],
            [/\\$/, 'string'],
        ],
        fStringReplacement: [
            [/\}/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
        ],

        operators: [
            [
                /(\*\*|\/\/|<<|>>|:=|<=|>=|==|!=|\+=|-=|\*-|\/=|\.\.=|%=|@=|&=|\|=|\^=|>>=|<<=|\*\*=|[+\-*/%@&|^~<>])/,
                'keyword.operator',
            ],
        ],

        attributes: [
            [/\b/, '@pop'],
            [/[a-zA-Z_]\w*/, 'attribute'],
        ],
    },
};

/**
 * Creates a new template for the given parameters.
 *
 * @param hubClassName The hub class name, e.g. `"MoveHub"`.
 * @param deviceClassNames A list of device class names, e.g. `["Motor"]`.
 * @returns The template.
 */
function createTemplate(hubClassName: string, deviceClassNames: string[]): string {
    return `from pybricks.hubs import ${hubClassName}
from pybricks.pupdevices import ${deviceClassNames.join(', ')}
from pybricks.parameters import Button, Color, Direction, Port, Side, Stop
from pybricks.robotics import DriveBase
from pybricks.tools import wait, StopWatch

hub = ${hubClassName}()

`;
}

type HubLabel =
    | 'movehub'
    | 'cityhub'
    | 'technichub'
    | 'inventorhub'
    | 'primehub'
    | 'essentialhub';

const templateSnippets: Array<
    Required<
        Pick<monaco.languages.CompletionItem, 'label' | 'documentation' | 'insertText'>
    > & { label: HubLabel }
> = [
    {
        label: 'technichub',
        documentation: 'Template for Technic hub program.',
        insertText: createTemplate('TechnicHub', ['Motor']),
    },
    {
        label: 'cityhub',
        documentation: 'Template for City hub program.',
        insertText: createTemplate('CityHub', ['DCMotor', 'Light']),
    },
    {
        label: 'movehub',
        documentation: 'Template for BOOST Move hub program.',
        insertText: createTemplate('MoveHub', ['Motor', 'ColorDistanceSensor']),
    },
    {
        label: 'inventorhub',
        documentation: 'Template for MINDSTORMS Robot Inventor hub program.',
        insertText: createTemplate('InventorHub', [
            'Motor',
            'ColorSensor',
            'UltrasonicSensor',
        ]),
    },
    {
        label: 'primehub',
        documentation: 'Template for SPIKE Prime program.',
        insertText: createTemplate('PrimeHub', [
            'Motor',
            'ColorSensor',
            'UltrasonicSensor',
            'ForceSensor',
        ]),
    },
    {
        label: 'essentialhub',
        documentation: 'Template for SPIKE Essential program.',
        insertText: createTemplate('EssentialHub', [
            'Motor',
            'ColorSensor',
            'ColorLightMatrix',
        ]),
    },
];

/**
 * Gets the template text for a Pybricks MicroPython file.
 * @param hub The hub label.
 */
export function getPybricksMicroPythonFileTemplate(
    hub: HubLabel | undefined,
): string | undefined {
    return templateSnippets.find((t) => t.label === hub)?.insertText;
}

export const templateSnippetCompletions = <monaco.languages.CompletionItemProvider>{
    provideCompletionItems: (model, position, _context, _token) => {
        // templates snippets are only available on the first line
        if (position.lineNumber !== 1) {
            return undefined;
        }

        const range = {
            startLineNumber: position.lineNumber,
            startColumn: 1,
            endLineNumber: position.lineNumber,
            endColumn: position.column,
        };

        const textUntilPosition = model.getValueInRange(range);

        const items = templateSnippets
            .filter((x) => x.label.startsWith(textUntilPosition))
            .map<monaco.languages.CompletionItem>((x) => ({
                detail: x.insertText,
                kind: <monaco.languages.CompletionItemKind.Snippet>27,
                range,
                ...x,
            }));

        if (!items) {
            return undefined;
        }

        return { suggestions: items };
    },
};

// old snippets from ace editor
// eslint-disable-next-line
const _unused = `
snippet imp
	import \${1:module}
snippet from
	from \${1:package} import \${2:module}
# Module Docstring
snippet docs
	'''
	File: \${1:FILENAME:file_name}
	Author: \${2:author}
	Date: \${3:date}
	Description: \${4}
	'''
snippet wh
	while \${1:condition}:
		\${2:# TODO: write code...}
# dowh - does the same as do...while in other languages
snippet dowh
	while True:
		\${1:# TODO: write code...}
		if \${2:condition}:
			break
snippet with
	with \${1:expr} as \${2:var}:
		\${3:# TODO: write code...}
# New Class
snippet cl
	class \${1:ClassName}(\${2:object}):
		"""\${3:docstring for $1}"""
		def __init__(self, \${4:arg}):
			\${5:super($1, self).__init__()}
			self.$4 = $4
			\${6}
# New Function
snippet def
	def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
		"""\${3:docstring for $1}"""
		\${4:# TODO: write code...}
snippet deff
	def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
		\${3:# TODO: write code...}
# New Method
snippet defs
	def \${1:mname}(self, \${2:arg}):
		\${3:# TODO: write code...}
# New Property
snippet property
	@property
	def \${1:pname}():
			\${2:return self._$1}
# Ifs
snippet if
	if \${1:condition}:
		\${2:# TODO: write code...}
snippet el
	else:
		\${1:# TODO: write code...}
snippet ei
	elif \${1:condition}:
		\${2:# TODO: write code...}
# For
snippet for
	for \${1:item} in \${2:items}:
		\${3:# TODO: write code...}
# Lambda
snippet ld
	\${1:var} = lambda \${2:vars} : \${3:action}
snippet .
	self.
snippet try Try/Except
	try:
		\${1:# TODO: write code...}
	except \${2:Exception} as \${3:e}:
		\${4:raise $3}
snippet try Try/Except/Else
	try:
		\${1:# TODO: write code...}
	except \${2:Exception} as \${3:e}:
		\${4:raise $3}
	else:
		\${5:# TODO: write code...}
snippet try Try/Except/Finally
	try:
		\${1:# TODO: write code...}
	except \${2:Exception} as \${3:e}:
		\${4:raise $3}
	finally:
		\${5:# TODO: write code...}
snippet try Try/Except/Else/Finally
	try:
		\${1:# TODO: write code...}
	except \${2:Exception} as \${3:e}:
		\${4:raise $3}
	else:
		\${5:# TODO: write code...}
	finally:
		\${6:# TODO: write code...}
snippet "
	"""
	\${1:doc}
	"""
`;
