/*
 * Copyright (C) 2017 The Dagger Authors.
 *
 * 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 dagger.internal.codegen.writing;

import static com.squareup.javapoet.MethodSpec.constructorBuilder;
import static com.squareup.javapoet.MethodSpec.methodBuilder;
import static com.squareup.javapoet.TypeSpec.classBuilder;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static javax.lang.model.element.Modifier.PRIVATE;

import androidx.room.compiler.processing.XElement;
import com.google.common.collect.ImmutableList;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import dagger.internal.codegen.base.SourceFileGenerator;
import javax.lang.model.element.Modifier;

/**
 * A source file generator that only writes the relevant code necessary for Bazel to create a
 * correct header (ABI) jar.
 */
public final class HjarSourceFileGenerator<T> extends SourceFileGenerator<T> {
  private final SourceFileGenerator<T> delegate;

  private HjarSourceFileGenerator(SourceFileGenerator<T> delegate) {
    super(delegate);
    this.delegate = delegate;
  }

  public static <T> SourceFileGenerator<T> wrap(SourceFileGenerator<T> delegate) {
    return new HjarSourceFileGenerator<>(delegate);
  }

  @Override
  public XElement originatingElement(T input) {
    return delegate.originatingElement(input);
  }

  @Override
  public ImmutableList<TypeSpec.Builder> topLevelTypes(T input) {
    return delegate.topLevelTypes(input).stream()
        .map(completeType -> skeletonType(completeType.build()))
        .collect(toImmutableList());
  }

  private TypeSpec.Builder skeletonType(TypeSpec completeType) {
    TypeSpec.Builder skeleton =
        classBuilder(completeType.name)
            .addSuperinterfaces(completeType.superinterfaces)
            .addTypeVariables(completeType.typeVariables)
            .addModifiers(completeType.modifiers.toArray(new Modifier[0]))
            .addAnnotations(completeType.annotations);

    if (!completeType.superclass.equals(ClassName.OBJECT)) {
      skeleton.superclass(completeType.superclass);
    }

    completeType.methodSpecs.stream()
        .filter(method -> !method.modifiers.contains(PRIVATE) || method.isConstructor())
        .map(this::skeletonMethod)
        .forEach(skeleton::addMethod);

    completeType.fieldSpecs.stream()
        .filter(field -> !field.modifiers.contains(PRIVATE))
        .map(this::skeletonField)
        .forEach(skeleton::addField);

    completeType.typeSpecs.stream()
        .map(type -> skeletonType(type).build())
        .forEach(skeleton::addType);

    return skeleton;
  }

  private MethodSpec skeletonMethod(MethodSpec completeMethod) {
    MethodSpec.Builder skeleton =
        completeMethod.isConstructor()
            ? constructorBuilder()
            : methodBuilder(completeMethod.name).returns(completeMethod.returnType);

    if (completeMethod.isConstructor()) {
      // Code in Turbine must (for technical reasons in javac) have a valid super() call for
      // constructors, otherwise javac will bark, and Turbine has no way to avoid this. So we retain
      // constructor method bodies if they do exist
      skeleton.addCode(completeMethod.code);
    }

    return skeleton
        .addModifiers(completeMethod.modifiers)
        .addTypeVariables(completeMethod.typeVariables)
        .addParameters(completeMethod.parameters)
        .addExceptions(completeMethod.exceptions)
        .varargs(completeMethod.varargs)
        .addAnnotations(completeMethod.annotations)
        .build();
  }

  private FieldSpec skeletonField(FieldSpec completeField) {
    return FieldSpec.builder(
            completeField.type,
            completeField.name,
            completeField.modifiers.toArray(new Modifier[0]))
        .addAnnotations(completeField.annotations)
        .build();
  }
}
