/**
 * Copyright 2011-2019 Asakusa Framework Team.
 *
 * 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.asakusafw.operator.util;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

import com.asakusafw.operator.CompileEnvironment;
import com.asakusafw.operator.description.AnnotationDescription;
import com.asakusafw.operator.description.ArrayDescription;
import com.asakusafw.operator.description.ArrayTypeDescription;
import com.asakusafw.operator.description.BasicTypeDescription;
import com.asakusafw.operator.description.ClassDescription;
import com.asakusafw.operator.description.EnumConstantDescription;
import com.asakusafw.operator.description.ImmediateDescription;
import com.asakusafw.operator.description.ObjectDescription;
import com.asakusafw.operator.description.ReifiableTypeDescription;
import com.asakusafw.operator.description.TypeDescription;
import com.asakusafw.operator.description.ValueDescription;
import com.asakusafw.utils.java.model.syntax.Annotation;
import com.asakusafw.utils.java.model.syntax.AnnotationElement;
import com.asakusafw.utils.java.model.syntax.ArrayCreationExpression;
import com.asakusafw.utils.java.model.syntax.ArrayType;
import com.asakusafw.utils.java.model.syntax.BasicTypeKind;
import com.asakusafw.utils.java.model.syntax.ClassLiteral;
import com.asakusafw.utils.java.model.syntax.Expression;
import com.asakusafw.utils.java.model.syntax.ModelFactory;
import com.asakusafw.utils.java.model.syntax.NamedType;
import com.asakusafw.utils.java.model.syntax.Type;
import com.asakusafw.utils.java.model.util.ImportBuilder;
import com.asakusafw.utils.java.model.util.Models;
import com.asakusafw.utils.java.model.util.TypeBuilder;

/**
 * Common helper methods about descriptions.
 * @since 0.9.0
 * @version 0.9.1
 */
public final class DescriptionHelper {

    private DescriptionHelper() {
        return;
    }

    /**
     * Returns the description.
     * @param environment the current environment
     * @param type the target type
     * @return the corresponded description
     */
    public static TypeDescription toDescription(CompileEnvironment environment, TypeMirror type) {
        switch (type.getKind()) {
        case DECLARED:
            return toDescription(environment, (javax.lang.model.type.DeclaredType) type);
        case ARRAY:
            return toDescription(environment, (javax.lang.model.type.ArrayType) type);
        case BOOLEAN:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.BOOLEAN);
        case BYTE:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.BYTE);
        case SHORT:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.SHORT);
        case INT:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.INT);
        case LONG:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.LONG);
        case FLOAT:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.FLOAT);
        case DOUBLE:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.DOUBLE);
        case CHAR:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.CHAR);
        case VOID:
            return new BasicTypeDescription(BasicTypeDescription.BasicTypeKind.VOID);
        default:
            throw new AssertionError(MessageFormat.format(
                    "unsupported type: {0}", //$NON-NLS-1$
                    type));
        }
    }

    /**
     * Returns the description.
     * @param environment the current environment
     * @param element the target element
     * @return the corresponded description
     */
    public static ClassDescription toDescription(CompileEnvironment environment, TypeElement element) {
        Name name = environment.getProcessingEnvironment().getElementUtils().getBinaryName(element);
        return new ClassDescription(name.toString());
    }

    /**
     * Returns the description.
     * @param environment the current environment
     * @param type the target type
     * @return the corresponded description
     */
    public static ClassDescription toDescription(
            CompileEnvironment environment,
            javax.lang.model.type.DeclaredType type) {
        TypeElement element = (TypeElement) type.asElement();
        return toDescription(environment, element);
    }

    private static TypeDescription toDescription(
            CompileEnvironment environment,
            javax.lang.model.type.ArrayType type) {
        TypeDescription element = toDescription(environment, type.getComponentType());
        return new ArrayTypeDescription(element);
    }

    /**
     * Returns the resolved type.
     * @param importer the current import builder
     * @param type the target class description
     * @return the resolved type
     */
    public static Type resolve(ImportBuilder importer, TypeDescription type) {
        switch (type.getTypeKind()) {
        case BASIC:
            return resolve((BasicTypeDescription) type);
        case CLASS:
            return resolve(importer, (ClassDescription) type);
        case ARRAY:
            return resolve(importer, (ArrayTypeDescription) type);
        default:
            throw new AssertionError(type);
        }
    }

    private static Type resolve(BasicTypeDescription type) {
        ModelFactory factory = Models.getModelFactory();
        switch (type.getBasicTypeKind()) {
        case BOOLEAN:
            return factory.newBasicType(BasicTypeKind.BOOLEAN);
        case BYTE:
            return factory.newBasicType(BasicTypeKind.BYTE);
        case CHAR:
            return factory.newBasicType(BasicTypeKind.CHAR);
        case DOUBLE:
            return factory.newBasicType(BasicTypeKind.DOUBLE);
        case FLOAT:
            return factory.newBasicType(BasicTypeKind.FLOAT);
        case INT:
            return factory.newBasicType(BasicTypeKind.INT);
        case LONG:
            return factory.newBasicType(BasicTypeKind.LONG);
        case SHORT:
            return factory.newBasicType(BasicTypeKind.SHORT);
        case VOID:
            return factory.newBasicType(BasicTypeKind.VOID);
        default:
            throw new AssertionError(type);
        }
    }

    private static Type resolve(ImportBuilder importer, ClassDescription type) {
        ModelFactory factory = Models.getModelFactory();
        return importer.toType(Models.toName(factory, type.getClassName()));
    }

    private static Type resolve(ImportBuilder importer, ArrayTypeDescription type) {
        int dim = 1;
        TypeDescription current = type.getComponentType();
        while (current.getTypeKind() == TypeDescription.TypeKind.ARRAY) {
            current = ((ArrayTypeDescription) current).getComponentType();
            dim++;
        }
        ModelFactory factory = Models.getModelFactory();
        Type result = resolve(importer, current);
        for (int i = 0; i < dim; i++) {
            result = factory.newArrayType(result);
        }
        return result;
    }

    /**
     * Returns the resolved value.
     * @param importer the current import builder
     * @param value the target constant value
     * @return the resolved expression
     */
    public static Expression resolveValue(ImportBuilder importer, ValueDescription value) {
        switch (value.getValueKind()) {
        case OBJECT:
            return resolveValue(importer, (ObjectDescription) value);
        case ARRAY:
            return resolveValue(importer, (ArrayDescription) value);
        default:
            return resolveConstant(importer, value);
        }
    }

    /**
     * Returns the resolved constant value.
     * @param importer the current import builder
     * @param constant the target constant value
     * @return the resolved expression
     */
    public static Expression resolveConstant(ImportBuilder importer, ValueDescription constant) {
        switch (constant.getValueKind()) {
        case IMMEDIATE:
            return resolveConstant((ImmediateDescription) constant);
        case ENUM_CONSTANT:
            return resolveConstant(importer, (EnumConstantDescription) constant);
        case TYPE:
            return resolveConstant(importer, (ReifiableTypeDescription) constant);
        case ARRAY:
            return resolveConstant(importer, (ArrayDescription) constant);
        default:
            throw new IllegalArgumentException(MessageFormat.format(
                    "not a constant value: {0}", //$NON-NLS-1$
                    constant));
        }
    }

    private static Expression resolveConstant(ImmediateDescription constant) {
        ModelFactory factory = Models.getModelFactory();
        Object value = constant.getValue();
        if (value == null) {
            return Models.toNullLiteral(factory);
        } else {
            return Models.toLiteral(factory, value);
        }
    }

    private static Expression resolveConstant(ImportBuilder importer, EnumConstantDescription constant) {
        ModelFactory factory = Models.getModelFactory();
        Type type = resolve(importer, constant.getDeclaringClass());
        return new TypeBuilder(factory, type).field(constant.getName()).toExpression();
    }

    private static ClassLiteral resolveConstant(ImportBuilder importer, ReifiableTypeDescription constant) {
        ModelFactory factory = Models.getModelFactory();
        return factory.newClassLiteral(resolve(importer, constant));
    }

    private static ArrayCreationExpression resolveConstant(ImportBuilder importer, ArrayDescription constant) {
        ModelFactory factory = Models.getModelFactory();
        ArrayType type = (ArrayType) resolve(importer, constant.getValueType());
        List<Expression> elements = new ArrayList<>();
        for (ValueDescription value : constant.getElements()) {
            elements.add(resolveConstant(importer, value));
        }
        return factory.newArrayCreationExpression(type, factory.newArrayInitializer(elements));
    }

    private static Expression resolveValue(ImportBuilder importer, ObjectDescription value) {
        ModelFactory factory = Models.getModelFactory();
        Type type = resolve(importer, value.getValueType());
        List<Expression> arguments = new ArrayList<>();
        for (ValueDescription argValue : value.getArguments()) {
            arguments.add(resolveValue(importer, argValue));
        }
        return Optional.ofNullable(value.getMethodName())
                .map(name -> new TypeBuilder(factory, type).method(name, arguments))
                .orElseGet(() -> new TypeBuilder(factory, type).newObject(arguments))
                .toExpression();
    }

    private static ArrayCreationExpression resolveValue(ImportBuilder importer, ArrayDescription value) {
        ModelFactory factory = Models.getModelFactory();
        ArrayType type = (ArrayType) resolve(importer, value.getValueType());
        List<Expression> elements = new ArrayList<>();
        for (ValueDescription elemValue : value.getElements()) {
            elements.add(resolveValue(importer, elemValue));
        }
        return factory.newArrayCreationExpression(type, factory.newArrayInitializer(elements));
    }

    /**
     * Returns the resolved annotation.
     * @param importer the current import builder
     * @param annotation the target annotation description
     * @return the resolved annotation
     */
    public static Annotation resolveAnnotation(ImportBuilder importer, AnnotationDescription annotation) {
        ModelFactory factory = Models.getModelFactory();
        NamedType type = (NamedType) resolve(importer, annotation.getDeclaringClass());
        Map<String, Expression> elements = new LinkedHashMap<>();
        for (Map.Entry<String, ValueDescription> entry : annotation.getElements().entrySet()) {
            String name = entry.getKey();
            Expression value = resolveAnnotationValue(importer, entry.getValue());
            elements.put(name, value);
        }
        if (elements.isEmpty()) {
            return factory.newMarkerAnnotation(type);
        } else if (elements.size() == 1 && elements.containsKey("value")) { //$NON-NLS-1$
            return factory.newSingleElementAnnotation(type, elements.get("value")); //$NON-NLS-1$
        } else {
            List<AnnotationElement> elementList = new ArrayList<>();
            for (Map.Entry<String, Expression> entry : elements.entrySet()) {
                String name = entry.getKey();
                Expression value = entry.getValue();
                elementList.add(factory.newAnnotationElement(factory.newSimpleName(name), value));
            }
            return factory.newNormalAnnotation(type, elementList);
        }
    }

    private static Expression resolveAnnotationValue(ImportBuilder importer, ValueDescription value) {
        switch (value.getValueKind()) {
        case IMMEDIATE:
        case TYPE:
        case ENUM_CONSTANT:
            return resolveConstant(importer, value);
        case ARRAY: {
            ModelFactory factory = Models.getModelFactory();
            List<Expression> elements = new ArrayList<>();
            for (ValueDescription element : ((ArrayDescription) value).getElements()) {
                elements.add(resolveAnnotationValue(importer, element));
            }
            if (elements.size() == 1) {
                return elements.get(0);
            }
            return factory.newArrayInitializer(elements);
        }
        case ANNOTATION:
            return resolveAnnotation(importer, (AnnotationDescription) value);
        default:
            throw new IllegalArgumentException(MessageFormat.format(
                    "not an annotation value: {0}", //$NON-NLS-1$
                    value));
        }
    }
}
