/*
 * Copyright (C) 2017 skydoves
 *
 * 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.skydoves.processor;

import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

import javax.lang.model.element.Modifier;

public class KickbackBoxGenerator {

    private final KickbackBoxAnnotatedClass annotatedClazz;

    private final String CLAZZ_PREFIX = "Kickback_";
    private final String FIELD_PREFIX = "kickback_";
    private final String SETTER_PREFIX = "set";
    private final String GETTER_PREFIX = "get";
    private final String FREE_PREFIX = "free";
    private final String FREE_ALL = "freeAll";

    public KickbackBoxGenerator(KickbackBoxAnnotatedClass annotatedClazz) {
        this.annotatedClazz = annotatedClazz;
    }

    public TypeSpec generate() {
        return TypeSpec.classBuilder(getClassName())
                .addJavadoc("Generated by Kickback. (https://github.com/skydoves/Kickback).\n")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(LifecycleObserver.class)
                .addFields(getKickbackFields())
                .addMethods(getSetterMethodSpecs())
                .addMethods(getGetterMethodSpecs())
                .addMethods(getFreeMethodSpecs())
                .addMethod(getBoxNameSpec())
                .addMethods(getElementNameListSpecs())
                .addMethod(getFreeAllSpec())
                .addMethod(setLifecycleObserverSpec())
                .addMethod(getLifecycleObserverSpec())
                .build();
    }

    private List<FieldSpec> getKickbackFields() {
        List<FieldSpec> fieldSpecList = new ArrayList<>();
        this.annotatedClazz.kickbackElementList.forEach(element -> {
            if(element.isWeak) {
                TypeName elementWild = WildcardTypeName.subtypeOf(element.typeName);
                TypeName weakTypeName = ParameterizedTypeName.get(ClassName.get(WeakReference.class), elementWild);
                FieldSpec.Builder builder = FieldSpec.builder(weakTypeName, getFieldName(element.elementName), Modifier.PRIVATE, Modifier.STATIC);
                if (element.value != null) {
                    if (element.isPrimitive) {
                        builder.initializer("new $T($L)", WeakReference.class, element.value);
                    } else if (element.value instanceof String) {
                        builder.initializer("new $T($S)", WeakReference.class,  element.value);
                    }
                }  else {
                    builder.initializer("new $T(null)", SoftReference.class);
                }
                fieldSpecList.add(builder.build());
            } else if(element.isSoft) {
                TypeName elementWild = WildcardTypeName.subtypeOf(element.typeName);
                TypeName softTypeName = ParameterizedTypeName.get(ClassName.get(SoftReference.class), elementWild);
                FieldSpec.Builder builder = FieldSpec.builder(softTypeName, getFieldName(element.elementName), Modifier.PRIVATE, Modifier.STATIC);
                if (element.value != null) {
                    if (element.isPrimitive) {
                        builder.initializer("new $T($L)", SoftReference.class, element.value);
                    } else if (element.value instanceof String) {
                        builder.initializer("new $T($S)", SoftReference.class,  element.value);
                    }
                } else {
                    builder.initializer("new $T(null)", SoftReference.class);
                }
                fieldSpecList.add(builder.build());
            }  else {
                FieldSpec.Builder builder = FieldSpec.builder(element.typeName, getFieldName(element.elementName), Modifier.PRIVATE, Modifier.STATIC);

                if (element.value != null) {
                    if (element.isPrimitive) {
                        builder.initializer("$L", element.value);
                    } else if (element.value instanceof String) {
                        builder.initializer("$S", element.value);
                    }
                }
                fieldSpecList.add(builder.build());
            }
        });
        return fieldSpecList;
    }

    private List<MethodSpec> getSetterMethodSpecs() {
        List<MethodSpec> setterSpecList = new ArrayList<>();
        this.annotatedClazz.kickbackElementList.forEach(element -> {
            if(element.isWeak) {
                MethodSpec setterSpec = MethodSpec.methodBuilder(getSetterPrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .addParameter(element.typeName, element.elementName.toLowerCase())
                        .addStatement("$N = new $T($N)", getFieldName(element.elementName), WeakReference.class, element.elementName.toLowerCase())
                        .build();
                setterSpecList.add(setterSpec);
            } else if(element.isSoft) {
                MethodSpec setterSpec = MethodSpec.methodBuilder(getSetterPrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .addParameter(element.typeName, element.elementName.toLowerCase())
                        .addStatement("$N = new $T($N)", getFieldName(element.elementName), SoftReference.class, element.elementName.toLowerCase())
                        .build();
                setterSpecList.add(setterSpec);
            } else {
                MethodSpec setterSpec = MethodSpec.methodBuilder(getSetterPrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .addParameter(element.typeName, element.elementName.toLowerCase())
                        .addStatement("$N = $N", getFieldName(element.elementName), element.elementName.toLowerCase())
                        .build();
                setterSpecList.add(setterSpec);
            }
        });
        return setterSpecList;
    }

    private List<MethodSpec> getGetterMethodSpecs() {
        List<MethodSpec> getterSpecList = new ArrayList<>();
        this.annotatedClazz.kickbackElementList.forEach(element -> {
            if(element.isWeak || element.isSoft) {
                MethodSpec getterSpec = MethodSpec.methodBuilder(getGetterPrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(element.typeName)
                        .addStatement("return $N.get()", getFieldName(element.elementName))
                        .build();
                getterSpecList.add(getterSpec);
            } else {
                MethodSpec getterSpec = MethodSpec.methodBuilder(getGetterPrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(element.typeName)
                        .addStatement("return $N", getFieldName(element.elementName))
                        .build();
                getterSpecList.add(getterSpec);
            }
        });
        return getterSpecList;
    }

    private List<MethodSpec> getFreeMethodSpecs() {
        List<MethodSpec> freeSpecList = new ArrayList<>();
        this.annotatedClazz.kickbackElementList.forEach(element -> {
            if(element.isWeak || element.isSoft) {
                MethodSpec freeSpec = MethodSpec.methodBuilder(getFreePrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .addStatement("$N.clear()", getFieldName(element.elementName))
                        .build();
                freeSpecList.add(freeSpec);
            } else {
                MethodSpec freeSpec = MethodSpec.methodBuilder(getFreePrefixName(element.elementName))
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .addStatement("$N = null", getFieldName(element.elementName))
                        .build();
                freeSpecList.add(freeSpec);
            }
        });
        return freeSpecList;
    }

    private MethodSpec getBoxNameSpec() {
        return MethodSpec.methodBuilder("getBoxName")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(String.class)
                .addStatement("return $S", this.annotatedClazz.boxName)
                .build();
    }

    private List<MethodSpec> getElementNameListSpecs() {
        List<MethodSpec> nameSpecList = new ArrayList<>();
        this.annotatedClazz.kickbackElementList.forEach(element -> {
            MethodSpec nameSped = MethodSpec.methodBuilder(String.format("get%sName", element.elementName))
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(String.class)
                    .addStatement("return $S", element.elementName)
                    .build();
            nameSpecList.add(nameSped);
        });
        return nameSpecList;
    }

    private MethodSpec getFreeAllSpec() {
         MethodSpec.Builder builder = MethodSpec.methodBuilder(FREE_ALL)
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC);

         this.annotatedClazz.kickbackElementList.forEach(element -> {

             if(element.isWeak || element.isSoft)
                 builder.addStatement("$N.clear()", getFieldName(element.elementName));
             else
                 builder.addStatement("$N = null", getFieldName(element.elementName));
         });

         return builder.build();
    }

    private MethodSpec setLifecycleObserverSpec() {
        return MethodSpec.methodBuilder("setLifecycleObserver")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(LifecycleOwner.class, "lifecycleOwner")
                .addStatement("lifecycleOwner.getLifecycle().addObserver(this)")
                .build();
    }

    private MethodSpec getLifecycleObserverSpec() {
        return MethodSpec.methodBuilder("getLifecycleObserver")
                .addModifiers(Modifier.PUBLIC)
                .returns(LifecycleObserver.class)
                .addStatement("return this")
                .build();
    }

    private String getClassName() {
        return CLAZZ_PREFIX + this.annotatedClazz.boxName;
    }

    private String getFieldName(String name) {
        return FIELD_PREFIX + name;
    }

    private String getSetterPrefixName(String name) {
        return SETTER_PREFIX + name;
    }

    private String getGetterPrefixName(String name) {
        return GETTER_PREFIX + name;
    }

    private String getFreePrefixName(String name) {
        return FREE_PREFIX + name;
    }
}