/*
 * Copyright (C) 2016 Sergej Shafarenka, www.halfbit.de
 *
 * 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 de.halfbit.featured.compiler;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;

import java.util.ArrayList;
import java.util.List;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import de.halfbit.featured.compiler.model.FeatureNode;
import de.halfbit.featured.compiler.model.MethodNode;

public class Names {

    public static final String PACKAGE_NAME = "de.halfbit.featured";

    private static final ClassName FEATURE =
            ClassName.get(PACKAGE_NAME, "Feature");
    private static final ClassName FEATURE_EVENT =
            ClassName.get(PACKAGE_NAME, "FeatureEvent");
    private static final ClassName FEATURE_HOST =
            ClassName.get(PACKAGE_NAME, "FeatureHost");
    private static final ClassName FEATURE_HOST_EVENT =
            ClassName.get(PACKAGE_NAME, "FeatureHost", "Event");
    private static final ClassName FEATURE_HOST_DISPATCH_COMPLETED =
            ClassName.get(PACKAGE_NAME, "FeatureHost", "OnDispatchCompleted");
    private static final ClassName CONTEXT =
            ClassName.get("android.content", "Context");
    private static final ClassName STRING =
            ClassName.get("java.lang", "String");
    private static final ClassName NOT_NULL =
            ClassName.get("org.jetbrains.annotations", "NotNull");
    private static final ClassName OVERRIDE =
            ClassName.get("java.lang", "Override");

    private static final int HOST_PARAMETER_INDEX = 0;
    private static final int CONTEXT_PARAMETER_INDEX = 1;

    private final Elements mElementUtils;
    private final Types mTypeUtils;

    public Names(Elements elementUtils, Types typeUtils) {
        mElementUtils = elementUtils;
        mTypeUtils = typeUtils;
    }

    private String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    public ClassName getFeatureClassName(FeatureNode featureNode) {
        TypeElement element = featureNode.getElement();
        return ClassName.get(getPackageName(element), element.getSimpleName().toString());
    }

    public ClassName getFeatureHostClassName(FeatureNode featureNode) {

        // we read host name from the feature class parameters

        TypeMirror superType = featureNode.getElement().getSuperclass();
        if (superType.getKind() != TypeKind.DECLARED) {
            throw new IllegalArgumentException();
        }

        TypeName featureHostName = getFeatureParameterTypeVariableName(
                (DeclaredType) superType, HOST_PARAMETER_INDEX);
        ClassName featureHostClassName = null;

        if (featureHostName instanceof ClassName) {
            // FeatureA extends Feature<FeatureAHost, Context>
            featureHostClassName = (ClassName) featureHostName;
        }

        if (featureHostName instanceof TypeVariableName) {
            // FeatureA<FH extends FeatureAHost, C extends Context> extends Feature<FH, C>
            TypeVariableName featureHostVariableTypeName = (TypeVariableName) featureHostName;
            if (featureHostVariableTypeName.bounds.size() <= HOST_PARAMETER_INDEX) {
                throw new IllegalArgumentException("Missing feature host parameter. \n"
                        + featureNode + "\n" + featureHostVariableTypeName);
            }
            TypeName featureHostTypeName = featureHostVariableTypeName.bounds
                    .get(HOST_PARAMETER_INDEX);
            if (featureHostTypeName instanceof ClassName) {
                featureHostClassName = (ClassName) featureHostTypeName;
            }
        }

        if (featureHostClassName == null) {
            throw new IllegalArgumentException("Unsupported feature host name declaration. \n"
                    + featureNode + "\n" + featureHostName);
        }

        PackageElement packageElement = mElementUtils.getPackageOf(featureNode.getElement());
        return ClassName.get(packageElement.toString(), featureHostClassName.simpleName());
    }

    public ClassName getFeatureContextSuperClassName(FeatureNode featureNode) {
        return getFeatureParameterClass(featureNode, CONTEXT_PARAMETER_INDEX);
    }

    private ClassName getFeatureParameterClass(FeatureNode featureNode, int parameterIndex) {
        TypeMirror superClass = featureNode.getElement().getSuperclass();
        if (superClass.getKind() != TypeKind.DECLARED) {
            throw new IllegalArgumentException(
                    "Check model validator. It must check feature super class.");
        }

        Element argElem = getFeatureParameterElement((DeclaredType) superClass, parameterIndex);
        if (argElem == null || argElem.getKind() == ElementKind.TYPE_PARAMETER) {
            return null;
        }
        return ClassName.get((TypeElement) argElem);
    }

    public Element getFeatureParameterElement(DeclaredType clazz, int parameterIndex) {
        List<? extends TypeMirror> args = clazz.getTypeArguments();
        if (parameterIndex >= args.size()) {
            return null;
        }
        return mTypeUtils.asElement(args.get(parameterIndex));
    }

    public TypeVariableName getFeatureContextTypeVariableName(FeatureNode featureNode) {
        return getParameterTypeVariableName(featureNode, CONTEXT_PARAMETER_INDEX);
    }

    public TypeVariableName getFeatureHostTypeVariableName(FeatureNode featureNode) {
        return getParameterTypeVariableName(featureNode, HOST_PARAMETER_INDEX);
    }

    private TypeVariableName getParameterTypeVariableName(FeatureNode featureNode,
                                                          int parameterIndex) {
        TypeMirror type = featureNode.getElement().asType();
        if (type.getKind() != TypeKind.DECLARED) {
            throw new IllegalArgumentException("FeatureNode type is not supported: " + featureNode);
        }
        Element paramElem = getFeatureParameterElement((DeclaredType) type, parameterIndex);
        if (paramElem.getKind() == ElementKind.TYPE_PARAMETER) {
            return TypeVariableName.get((TypeVariable) paramElem.asType());
        }
        throw new IllegalArgumentException(
                "Expecting paramElem of kind TYPE_PARAMETER, while received" + paramElem);
    }

    public ClassName getNonNullClassName() {
        return NOT_NULL;
    }

    public ClassName getStringClassName() {
        return STRING;
    }

    public ClassName getOverrideClassName() {
        return OVERRIDE;
    }

    public ClassName getFeatureClassName() {
        return FEATURE;
    }

    public ClassName getFeatureEventClassName() {
        return FEATURE_EVENT;
    }

    public String getDispatchMethodName(MethodNode methodElement) {
        String methodName = methodElement.getElement().getSimpleName().toString();
        return "dispatch" + capitalize(methodName);
    }

    public static String capitalize(String text) {
        return text.substring(0, 1).toUpperCase() + text.substring(1, text.length());
    }

    public ClassName getEventClassName(MethodNode methodElement) {
        ClassName parentName = getFeatureHostClassName(methodElement.getParent());
        String eventName = getEventName(methodElement);
        return ClassName.get(parentName.packageName(), parentName.simpleName(), eventName);
    }

    private String getEventName(MethodNode methodElement) {
        return capitalize(getFeatureMethodName(methodElement)) + "Event";
    }

    public TypeName getEventSuperClassName() {
        return FEATURE_HOST_EVENT;
    }

    public String getFeatureMethodName(MethodNode methodElement) {
        return methodElement.getElement().getSimpleName().toString();
    }

    public TypeName getSuggestedSuperFeatureTypeName(FeatureNode featureNode) {
        String featureHostPackage = getPackageName(featureNode.getElement());
        String featureHostName = featureNode.getName() + "Host";
        ClassName suggestedFeatureHostClassName = ClassName
                .get(featureHostPackage, featureHostName);
        return ParameterizedTypeName.get(FEATURE, suggestedFeatureHostClassName, CONTEXT);
    }

    public TypeName getFeatureHostSuperTypeName(FeatureNode featureNode) {

        // public class FeatureA extends Feature<FeatureAHost, Context> {
        // public class FeatureA<FH extends FeatureAHost, C extends App> extends Feature<FH, C> {
        // public class FeatureB extends FeatureA<FeatureBHost, Context> {

        TypeMirror superType = featureNode.getElement().getSuperclass();
        if (superType.getKind() != TypeKind.DECLARED) {
            throw new IllegalArgumentException();
        }

        TypeName hostTypeName = getFeatureParameterTypeVariableName(
                (DeclaredType) superType, HOST_PARAMETER_INDEX);

        TypeName contextTypeName = getFeatureParameterTypeVariableName(
                (DeclaredType) superType, CONTEXT_PARAMETER_INDEX);

        FeatureNode superFeatureNode = featureNode.getSuperFeatureNode();
        if (superFeatureNode == null) {
            DeclaredType declaredType = (DeclaredType) superType;
            TypeName featureClassName = ClassName.get(declaredType.asElement().asType());
            if (featureClassName instanceof ParameterizedTypeName) {
                ParameterizedTypeName ptn = (ParameterizedTypeName) featureClassName;
                ClassName featureHostClass = ClassName.get(
                        ptn.rawType.packageName(), ptn.rawType.simpleName() + "Host");
                return ParameterizedTypeName.get(featureHostClass, hostTypeName, contextTypeName);
            }
        }

        ClassName featureHostClass = superFeatureNode == null
                ? FEATURE_HOST : getFeatureHostClassName(superFeatureNode);

        return ParameterizedTypeName.get(featureHostClass, hostTypeName, contextTypeName);
    }

    public TypeName getFeatureHostParameterTypeName(FeatureNode featureNode) {
        TypeMirror type = featureNode.getElement().asType();
        if (type.getKind() != TypeKind.DECLARED) {
            throw new IllegalArgumentException();
        }
        return getFeatureParameterTypeVariableName((DeclaredType) type, HOST_PARAMETER_INDEX);
    }

    private TypeName getFeatureParameterTypeVariableName(DeclaredType featureType,
                                                         int featureParameterIndex) {
        Element paramElem = getFeatureParameterElement(featureType, featureParameterIndex);
        if (paramElem == null) {
            return null;
        }

        if (paramElem.getKind() == ElementKind.TYPE_PARAMETER) {
            return TypeVariableName.get((TypeVariable) paramElem.asType());

        } else if (paramElem.getKind() == ElementKind.CLASS) {
            return TypeName.get(paramElem.asType());
        }

        return null;
    }

    public TypeName getTypeNameByKind(VariableElement param) {
        switch (param.asType().getKind()) {

            case BOOLEAN:
                return TypeName.BOOLEAN;
            case BYTE:
                return TypeName.BYTE;
            case CHAR:
                return TypeName.CHAR;
            case DOUBLE:
                return TypeName.DOUBLE;
            case FLOAT:
                return TypeName.FLOAT;
            case INT:
                return TypeName.INT;
            case LONG:
                return TypeName.LONG;
            case SHORT:
                return TypeName.SHORT;

            case DECLARED:
                TypeMirror type = param.asType();
                TypeName typeName = ClassName.get(type);
                typeName = applyAnnotations(typeName, param);
                return typeName;

            case ARRAY:
                ArrayType arrayType = (ArrayType) param.asType();
                TypeName arrayTypeName = ArrayTypeName.get(arrayType);
                arrayTypeName = applyAnnotations(arrayTypeName, param);
                return arrayTypeName;

            default:
                throw new IllegalStateException("unsupported kind: " + param.asType().getKind());
        }
    }

    private static TypeName applyAnnotations(TypeName typeName, VariableElement param) {
        List<? extends AnnotationMirror> annotationMirrors = param.getAnnotationMirrors();
        if (annotationMirrors.size() > 0) {
            List<AnnotationSpec> annotationSpecs =
                    new ArrayList<>(annotationMirrors.size());
            for (AnnotationMirror annotationMirror : annotationMirrors) {
                annotationSpecs.add(AnnotationSpec.get(annotationMirror));
            }
            typeName = typeName.annotated(annotationSpecs);
        }
        return typeName;
    }

    public ClassName getDispatchCompletedClassName() {
        return FEATURE_HOST_DISPATCH_COMPLETED;
    }

}