/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.facebook.litho.specmodels.processor;

import com.facebook.litho.annotations.PropDefault;
import com.facebook.litho.annotations.ResType;
import com.facebook.litho.specmodels.internal.ImmutableList;
import com.facebook.litho.specmodels.model.PropDefaultModel;
import com.squareup.javapoet.TypeName;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;

/** Extracts prop defaults from the given input. */
public class PropDefaultsExtractor {

  /** Get the prop defaults from the given {@link TypeElement}. */
  public static ImmutableList<PropDefaultModel> getPropDefaults(TypeElement typeElement) {
    final List<PropDefaultModel> propDefaults = new ArrayList<>();

    final List<? extends Element> enclosedElements = typeElement.getEnclosedElements();
    for (Element enclosedElement : enclosedElements) {
      propDefaults.addAll(extractFromField(enclosedElement));
      propDefaults.addAll(extractFromMethod(enclosedElement));
    }

    return ImmutableList.copyOf(propDefaults);
  }

  private static ImmutableList<PropDefaultModel> extractFromField(Element enclosedElement) {
    if (enclosedElement.getKind() != ElementKind.FIELD) {
      return ImmutableList.of();
    }

    final VariableElement variableElement = (VariableElement) enclosedElement;
    final Annotation propDefaultAnnotation = variableElement.getAnnotation(PropDefault.class);
    if (propDefaultAnnotation == null) {
      return ImmutableList.of();
    }

    final ResType propDefaultResType = ((PropDefault) propDefaultAnnotation).resType();
    final int propDefaultResId = ((PropDefault) propDefaultAnnotation).resId();

    return ImmutableList.of(
        new PropDefaultModel(
            TypeName.get(variableElement.asType()),
            variableElement.getSimpleName().toString(),
            ImmutableList.copyOf(new ArrayList<>(variableElement.getModifiers())),
            variableElement,
            propDefaultResType,
            propDefaultResId));
  }

  /**
   * This attempts to extract a prop-default from a <em>method</em>. This is only necessary for
   * Kotlin KAPT generated code, which currently does a rather strange thing where it generates a
   * method <code>void field_name$annotations()</code> for every <code>field_name</code> that has
   * all annotations for said field.
   *
   * <p>So, if we find a method that looks like this, and it contains a <code>PropDefault</code>
   * annotation, we will try to find a matching field of this name and add use it as basis for our
   * prop-default.
   */
  private static ImmutableList<PropDefaultModel> extractFromMethod(Element enclosedElement) {
    if (enclosedElement.getKind() != ElementKind.METHOD) {
      return ImmutableList.of();
    }

    final ExecutableElement methodElement = (ExecutableElement) enclosedElement;

    final Annotation propDefaultAnnotation = methodElement.getAnnotation(PropDefault.class);
    if (propDefaultAnnotation == null) {
      return ImmutableList.of();
    }

    final String methodName = methodElement.getSimpleName().toString();

    boolean isPropDefaultWithoutGet =
        methodName.endsWith("$annotations")
            && methodElement.getReturnType().getKind() == TypeKind.VOID;

    final String baseName;

    /*
     * In case an [@PropDefault] annotated variable does not include `get` on the Kotlin
     * annotation, we fallback to the previous method of identifying `PropDefault` values.
     * Note here that this method is deprecated and might be removed from KAPT some time in
     * future.
     *
     * If a user annotates that variable with `@get:PropDefault` we identify the
     * `PropDefault` values through the accompanying `get` method of that variable.
     * */
    if (isPropDefaultWithoutGet) {
      baseName = methodName.subSequence(0, methodName.indexOf('$')).toString();
    } else {
      baseName =
          methodName.replaceFirst("get", "").substring(0, 1).toLowerCase()
              + methodName.replaceFirst("get", "").substring(1);
    }

    final Optional<? extends Element> element =
        enclosedElement.getEnclosingElement().getEnclosedElements().stream()
            .filter(e -> e.getSimpleName().toString().equals(baseName))
            .findFirst();

    final ResType propDefaultResType = ((PropDefault) propDefaultAnnotation).resType();
    final int propDefaultResId = ((PropDefault) propDefaultAnnotation).resId();

    return element
        .map(
            e ->
                ImmutableList.of(
                    new PropDefaultModel(
                        TypeName.get(e.asType()),
                        baseName,
                        ImmutableList.copyOf(new ArrayList<>(methodElement.getModifiers())),
                        methodElement,
                        propDefaultResType,
                        propDefaultResId)))
        .orElseGet(ImmutableList::of);
  }
}

@@ -5392,16 +5392,18 @@
     e -%3E
+ %7B
 %0A       
@@ -5413,122 +5413,651 @@
     
-  ImmutableList.of(%0A                    new PropDefaultModel(%0A                        TypeName.get(e.asType()),%0A  
+final TypeName typeName;%0A              try %7B%0A                typeName = TypeName.get(e.asType());%0A              %7D catch (IllegalArgumentException ex) %7B%0A                throw new RuntimeException(%0A                    %22Failed to get a type name from @PropDefault annotated field %22%0A                        + e%0A                        + %22. IF THIS IS A KOTLIN FILE, THIS IS A KNOWN PROBLEM WITH USING KAPT. Luckily, you can fix it by annotating with @get:PropDefault instead of @PropDefault.%22,%0A                    ex);%0A              %7D%0A%0A              return ImmutableList.of(%0A                  new PropDefaultModel(%0A                      typeName,%0A
     
@@ -6080,26 +6080,24 @@
   baseName,%0A
-  
             
@@ -6193,26 +6193,24 @@
             
-  
 methodElemen
@@ -6226,34 +6226,32 @@
                 
- 
- 
 propDefaultResTy
@@ -6268,34 +6268,32 @@
                 
-  
 propDefaultResId
@@ -6294,16 +6294,31 @@
 tResId))
+;%0A            %7D
 )%0A      

Improve error message when failing to compile @PropDefault on kotlin MountSpec