/*
 * Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.pkl.core.generator;

import com.palantir.javapoet.CodeBlock;
import com.palantir.javapoet.JavaFile;
import com.palantir.javapoet.MethodSpec;
import com.palantir.javapoet.TypeName;
import com.palantir.javapoet.TypeSpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.pkl.parser.Parser;
import org.pkl.parser.syntax.Modifier.ModifierValue;

public final class BaseModuleMembersGenerator {
  record Members(Set<String> properties, Set<String> methods) {}

  public static void main(String[] args) {
    if (args.length < 2) {
      throw new IllegalArgumentException(
          "Usage: BaseModuleMembersGenerator <path-to-base.pkl> <output-dir>");
    }
    var members = buildMembers(args[0]);
    generateJavaCode(members, args[1]);
  }

  private static void generateJavaCode(Members members, String outputDir) {
    var privateConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build();

    var hasPropertyMethod =
        buildHasMethod("hasProperty", members.properties().stream().sorted().toList());
    var hasMethodMethod = buildHasMethod("hasMethod", members.methods().stream().sorted().toList());

    var classSpec =
        TypeSpec.classBuilder("BaseModuleMembers")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(privateConstructor)
            .addMethod(hasPropertyMethod)
            .addMethod(hasMethodMethod)
            .build();

    var javaFile =
        JavaFile.builder("org.pkl.core.runtime", classSpec)
            .addFileComment("DO NOT EDIT — generated by BaseModuleMembersGenerator")
            .build();

    try {
      javaFile.writeTo(Path.of(outputDir));
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  private static MethodSpec buildHasMethod(String methodName, List<String> names) {
    var code = CodeBlock.builder();
    code.add("return switch (name) {\n");
    code.indent();
    code.add("case $S", names.get(0));
    if (names.size() == 1) {
      code.add(" -> true;\n");
    } else {
      code.add(",\n");
      code.indent();
      for (var i = 1; i < names.size() - 1; i++) {
        code.add("$S,\n", names.get(i));
      }
      code.add("$S -> true;\n", names.get(names.size() - 1));
      code.unindent();
    }
    code.add("default -> false;\n");
    code.unindent();
    code.add("};\n");

    return MethodSpec.methodBuilder(methodName)
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(TypeName.BOOLEAN)
        .addParameter(String.class, "name")
        .addCode(code.build())
        .build();
  }

  private static String getBaseModuleText(String path) {
    try {
      return Files.readString(Path.of(path));
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  private static Members buildMembers(String basePklPath) {
    var text = getBaseModuleText(basePklPath);
    var parsed = new Parser().parseModule(text);
    var properties = new HashSet<String>();
    var methods = new HashSet<String>();
    for (var property : parsed.getProperties()) {
      if (isLocal(property.getModifiers())) {
        continue;
      }
      properties.add(property.getName().getValue());
    }
    for (var clazz : parsed.getClasses()) {
      if (isLocal(clazz.getModifiers())) {
        continue;
      }
      properties.add(clazz.getName().getValue());
    }
    for (var typealias : parsed.getTypeAliases()) {
      if (isLocal(typealias.getModifiers())) {
        continue;
      }
      properties.add(typealias.getName().getValue());
    }
    for (var method : parsed.getMethods()) {
      if (isLocal(method.getModifiers())) {
        continue;
      }
      methods.add(method.getName().getValue());
    }
    return new Members(properties, methods);
  }

  private static boolean isLocal(List<org.pkl.parser.syntax.Modifier> modifiers) {
    return modifiers.stream().anyMatch((it) -> it.getValue() == ModifierValue.LOCAL);
  }
}
