// Copyright 2022 Google LLC
//
// 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
//
//      http://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 com.google.api.generator.gapic.composer.samplecode;

import com.google.api.generator.engine.ast.AssignmentExpr;
import com.google.api.generator.engine.ast.ClassDefinition;
import com.google.api.generator.engine.ast.CommentStatement;
import com.google.api.generator.engine.ast.Expr;
import com.google.api.generator.engine.ast.ExprStatement;
import com.google.api.generator.engine.ast.MethodDefinition;
import com.google.api.generator.engine.ast.MethodInvocationExpr;
import com.google.api.generator.engine.ast.ScopeNode;
import com.google.api.generator.engine.ast.Statement;
import com.google.api.generator.engine.ast.TypeNode;
import com.google.api.generator.engine.ast.Variable;
import com.google.api.generator.engine.ast.VariableExpr;
import com.google.api.generator.gapic.composer.comment.CommentComposer;
import com.google.api.generator.gapic.model.RegionTag;
import com.google.api.generator.gapic.model.Sample;
import com.google.api.generator.gapic.utils.JavaStyle;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SampleComposer {
  static List<Statement> composeInlineSample(List<Statement> sampleBody) {
    return bodyWithComment(CommentComposer.AUTO_GENERATED_SAMPLE_COMMENT, sampleBody);
  }

  //  "Executable" meaning it includes the necessary code to execute java code,
  //  still may require additional configuration to actually execute generated sample code
  static ClassDefinition composeExecutableSample(Sample sample, String pakkage) {
    return createExecutableSample(
        sample.fileHeader(),
        pakkage,
        sample.name(),
        sample.variableAssignments(),
        bodyWithComment(CommentComposer.AUTO_GENERATED_SAMPLE_COMMENT, sample.body()),
        sample.regionTag());
  }

  private static List<Statement> bodyWithComment(
      List<Statement> autoGeneratedComment, List<Statement> sampleBody) {
    List<Statement> bodyWithComment = new ArrayList<>(autoGeneratedComment);
    bodyWithComment.addAll(sampleBody);
    return bodyWithComment;
  }

  private static ClassDefinition createExecutableSample(
      List<CommentStatement> fileHeader,
      String packageName,
      String sampleClassName,
      List<AssignmentExpr> sampleVariableAssignments,
      List<Statement> sampleBody,
      RegionTag regionTag) {

    String sampleMethodName = JavaStyle.toLowerCamelCase(sampleClassName);
    List<VariableExpr> sampleMethodArgs = composeSampleMethodArgs(sampleVariableAssignments);
    MethodDefinition mainMethod =
        composeMainMethod(
            composeMainBody(
                sampleVariableAssignments,
                composeInvokeMethodStatement(sampleMethodName, sampleMethodArgs)));
    MethodDefinition sampleMethod =
        composeSampleMethod(sampleMethodName, sampleMethodArgs, sampleBody);
    return composeSampleClass(
        fileHeader, packageName, sampleClassName, mainMethod, sampleMethod, regionTag);
  }

  private static List<VariableExpr> composeSampleMethodArgs(
      List<AssignmentExpr> sampleVariableAssignments) {
    return sampleVariableAssignments.stream()
        .map(v -> v.variableExpr().toBuilder().setIsDecl(true).build())
        .collect(Collectors.toList());
  }

  private static Statement composeInvokeMethodStatement(
      String sampleMethodName, List<VariableExpr> sampleMethodArgs) {
    List<Expr> invokeArgs =
        sampleMethodArgs.stream()
            .map(arg -> arg.toBuilder().setIsDecl(false).build())
            .collect(Collectors.toList());
    return ExprStatement.withExpr(
        MethodInvocationExpr.builder()
            .setMethodName(sampleMethodName)
            .setArguments(invokeArgs)
            .build());
  }

  private static List<Statement> composeMainBody(
      List<AssignmentExpr> sampleVariableAssignments, Statement invokeMethod) {
    List<ExprStatement> setVariables =
        sampleVariableAssignments.stream()
            .map(var -> ExprStatement.withExpr(var))
            .collect(Collectors.toList());
    List<Statement> body = new ArrayList<>(setVariables);
    body.add(invokeMethod);
    return body;
  }

  private static ClassDefinition composeSampleClass(
      List<CommentStatement> fileHeader,
      String packageName,
      String sampleClassName,
      MethodDefinition mainMethod,
      MethodDefinition sampleMethod,
      RegionTag regionTag) {
    return ClassDefinition.builder()
        .setFileHeader(fileHeader)
        .setRegionTag(regionTag)
        .setScope(ScopeNode.PUBLIC)
        .setPackageString(packageName)
        .setName(sampleClassName)
        .setMethods(ImmutableList.of(mainMethod, sampleMethod))
        .build();
  }

  private static MethodDefinition composeMainMethod(List<Statement> mainBody) {
    return MethodDefinition.builder()
        .setScope(ScopeNode.PUBLIC)
        .setIsStatic(true)
        .setReturnType(TypeNode.VOID)
        .setName("main")
        .setArguments(
            VariableExpr.builder()
                .setVariable(
                    Variable.builder().setType(TypeNode.STRING_ARRAY).setName("args").build())
                .setIsDecl(true)
                .build())
        .setThrowsExceptions(Arrays.asList(TypeNode.withExceptionClazz(Exception.class)))
        .setBody(mainBody)
        .build();
  }

  private static MethodDefinition composeSampleMethod(
      String sampleMethodName,
      List<VariableExpr> sampleMethodArgs,
      List<Statement> sampleMethodBody) {
    return MethodDefinition.builder()
        .setScope(ScopeNode.PUBLIC)
        .setIsStatic(true)
        .setReturnType(TypeNode.VOID)
        .setName(sampleMethodName)
        .setArguments(sampleMethodArgs)
        .setThrowsExceptions(Arrays.asList(TypeNode.withExceptionClazz(Exception.class)))
        .setBody(sampleMethodBody)
        .build();
  }
}
