// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.xpath.context.functions;

import com.intellij.lexer.FilterLexer;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.intellij.lang.xpath.XPath2TokenTypes;
import org.intellij.lang.xpath.XPathLexer;
import org.intellij.lang.xpath.XPathTokenTypes;
import org.intellij.lang.xpath.psi.XPath2SequenceType;
import org.intellij.lang.xpath.psi.XPath2Type;
import org.intellij.lang.xpath.psi.XPathType;
import org.jetbrains.annotations.Nullable;

import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class FunctionDeclarationParsing {
  public static final String FUNCTION_NAMESPACE = "http://www.w3.org/2005/xpath-functions";

  private FunctionDeclarationParsing() {
  }

  public static Pair<String, ? extends Function> parseFuntionDeclaration(String decl) {
    final Lexer lexer = new FilterLexer(XPathLexer.create(true),
            new FilterLexer.SetFilter(TokenSet.create(XPathTokenTypes.WHITESPACE)));
    lexer.start(decl);

    String prefix = "";
    if (lexer.getTokenType() == XPathTokenTypes.EXT_PREFIX) {
      prefix = lexer.getTokenText();
      lexer.advance();
      match(lexer, XPathTokenTypes.COL);
    }

    final String name = match(lexer, XPathTokenTypes.FUNCTION_NAME);
    match(lexer, XPathTokenTypes.LPAREN);

    final List<Parameter> parameters = new ArrayList<>();
    while (lexer.getTokenType() != XPathTokenTypes.RPAREN) {
      if (lexer.getTokenType() == XPathTokenTypes.DOTDOT) {
        lexer.advance();
        match(lexer, XPathTokenTypes.DOT);

        parameters.add(new Parameter(XPathType.ANY, Parameter.Kind.VARARG));
      } else {
        match(lexer, XPathTokenTypes.DOLLAR);
        match(lexer, XPathTokenTypes.VARIABLE_NAME);

        match(lexer, XPath2TokenTypes.AS);

        final String type = parseType(lexer);

        final XPath2SequenceType.Cardinality indicator = parseCardinality(lexer);

        parameters.add(new Parameter(mapType(type, indicator), Parameter.Kind.REQUIRED));
      }

      if (lexer.getTokenType() == XPathTokenTypes.COMMA) {
        lexer.advance();
      }
    }
    lexer.advance();

    match(lexer, XPath2TokenTypes.AS);

    final String ret = parseType(lexer);
    final XPath2SequenceType.Cardinality indicator = parseCardinality(lexer);

    final XPathType returnType = mapType(ret, indicator);

    return Pair.create(prefix, new FunctionImpl(name, returnType, parameters.toArray(new Parameter[0])));
  }

  public static XPathType mapType(String type, XPath2SequenceType.Cardinality c) {
    if ("none".equals(type)) {
      return XPathType.UNKNOWN;
    }

    XPathType r = null;
    if ("numeric".equals(type)) {
      r = XPath2Type.NUMERIC;
    } else if (type.startsWith("xs:")) {
      final String base = type.substring(3);
      r = XPath2Type.fromName(new QName(XPath2Type.XMLSCHEMA_NS, base));
      if (r == null) {
        r = XPathType.fromString(base);
      }
    } else {
      if (type.endsWith("()")) {
        r = XPath2Type.fromName(new QName("", type));
      }
    }

    if (r != null) {
      if (c != null) {
        r = XPath2SequenceType.create(r, c);
      }
      return r;
    }
    return XPathType.fromString(type);
  }

  public static @Nullable XPath2SequenceType.Cardinality parseCardinality(Lexer lexer) {
    if (lexer.getTokenType() == XPath2TokenTypes.QUEST) {
      lexer.advance();
      return XPath2SequenceType.Cardinality.OPTIONAL;
    } else if (lexer.getTokenType() == XPathTokenTypes.MULT || lexer.getTokenType() == XPathTokenTypes.STAR) {
      lexer.advance();
      return XPath2SequenceType.Cardinality.ZERO_OR_MORE;
    } else if (lexer.getTokenType() == XPathTokenTypes.PLUS) {
      lexer.advance();
      return XPath2SequenceType.Cardinality.ONE_OR_MORE;
    }
    return null;
  }

  public static String match(Lexer lexer, IElementType token) {
    assert lexer.getTokenType() == token : lexer.getTokenType() + ": " + lexer.getTokenText();
    final String s = lexer.getTokenText();
    lexer.advance();
    return s;
  }

  public static @Nullable String parseType(Lexer lexer) {
    String type = parseQName(lexer);
    if (type == null) {
      if (lexer.getTokenType() == XPath2TokenTypes.ITEM || lexer.getTokenType() == XPathTokenTypes.FUNCTION_NAME || lexer.getTokenType() == XPathTokenTypes.NODE_TYPE) {
        type = lexer.getTokenText();
        lexer.advance();
        match(lexer, XPathTokenTypes.LPAREN);
        match(lexer, XPathTokenTypes.RPAREN);
        type += "()";
      } else {
        assert false : "unexpected token: " + lexer.getTokenType();
      }
    }
    return type;
  }

  public static @Nullable String parseQName(Lexer lexer) {
    String name;
    if (lexer.getTokenType() == XPathTokenTypes.NCNAME) {
      name = lexer.getTokenText();
      lexer.advance();
      if (lexer.getTokenType() == XPathTokenTypes.COL) {
        lexer.advance();
        assert lexer.getTokenType() == XPathTokenTypes.NCNAME;
        name += (":" + lexer.getTokenText());
        lexer.advance();
      }
    } else {
      name = null;
    }
    return name;
  }

  public static void addFunction(Map<Pair<QName, Integer>, Function> decls, String s) {
    final Pair<String, ? extends Function> pair = parseFuntionDeclaration(s);
    final Function func = pair.second;

    final boolean fn = pair.first.equals("fn") || pair.first.isEmpty();
    final boolean xs = pair.first.equals("xs");
    final Pair<QName, Integer> key = Pair.create(new QName(fn ? FUNCTION_NAMESPACE : (xs ? XPath2Type.XMLSCHEMA_NS : null), func.getName()), func.getParameters().length);
    assert !decls.containsKey(key) : key;

    decls.put(key, func);

    if (fn) {
      decls.put(Pair.create(new QName(null, func.getName()), func.getParameters().length), func);
    }
  }
}