/*
 * Copyright 2010 the original author or 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 org.gradle.api.internal.project.taskfactory;

import com.google.common.collect.Maps;
import org.apache.commons.lang.StringUtils;
import org.gradle.api.*;
import org.gradle.api.internal.AbstractTask;
import org.gradle.api.internal.ConventionTask;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.changedetection.TaskArtifactState;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.internal.tasks.ContextAwareTaskAction;
import org.gradle.api.internal.tasks.TaskExecutionContext;
import org.gradle.api.internal.tasks.execution.TaskValidator;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.*;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import org.gradle.internal.Factory;
import org.gradle.internal.reflect.Instantiator;
import org.gradle.internal.reflect.JavaReflectionUtil;
import org.gradle.util.DeprecationLogger;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.Callable;

/**
 * A {@link ITaskFactory} which determines task actions, inputs and outputs based on annotation attached to the task properties. Also provides some validation based on these annotations.
 */
public class AnnotationProcessingTaskFactory implements ITaskFactory {
    private final ITaskFactory taskFactory;
    private final Map<Class, TaskClassInfo> classInfos;

    private final Transformer<Iterable<File>, Object> filePropertyTransformer = new Transformer<Iterable<File>, Object>() {
        public Iterable<File> transform(Object original) {
            File file = (File) original;
            return file == null ? Collections.<File>emptyList() : Collections.singleton(file);
        }
    };

    private final Transformer<Iterable<File>, Object> iterableFilePropertyTransformer = new Transformer<Iterable<File>, Object>() {
        @SuppressWarnings("unchecked")
        public Iterable<File> transform(Object original) {
            return original != null ? (Iterable<File>) original : Collections.<File>emptyList();
        }
    };

    private final List<? extends PropertyAnnotationHandler> handlers = Arrays.asList(
            new InputFilePropertyAnnotationHandler(),
            new InputDirectoryPropertyAnnotationHandler(),
            new InputFilesPropertyAnnotationHandler(),
            new OutputFilePropertyAnnotationHandler(OutputFile.class, filePropertyTransformer),
            new OutputFilePropertyAnnotationHandler(OutputFiles.class, iterableFilePropertyTransformer),
            new OutputDirectoryPropertyAnnotationHandler(OutputDirectory.class, filePropertyTransformer),
            new OutputDirectoryPropertyAnnotationHandler(OutputDirectories.class, iterableFilePropertyTransformer),
            new InputPropertyAnnotationHandler(),
            new NestedBeanPropertyAnnotationHandler());
    private final ValidationAction notNullValidator = new ValidationAction() {
        public void validate(String propertyName, Object value, Collection<String> messages) {
            if (value == null) {
                messages.add(String.format("No value has been specified for property '%s'.", propertyName));
            }
        }
    };

    public AnnotationProcessingTaskFactory(ITaskFactory taskFactory) {
        this.taskFactory = taskFactory;
        this.classInfos = new HashMap<Class, TaskClassInfo>();
    }

    private AnnotationProcessingTaskFactory(Map<Class, TaskClassInfo> classInfos, ITaskFactory taskFactory) {
        this.classInfos = classInfos;
        this.taskFactory = taskFactory;
    }

    public ITaskFactory createChild(ProjectInternal project, Instantiator instantiator) {
        return new AnnotationProcessingTaskFactory(classInfos, taskFactory.createChild(project, instantiator));
    }

    public TaskInternal createTask(Map<String, ?> args) {
        return process(taskFactory.createTask(args));
    }

    @Override
    public <S extends TaskInternal> S create(String name, Class<S> type) {
        return process(taskFactory.create(name, type));
    }

    private <S extends TaskInternal> S process(S task) {
        TaskClassInfo taskClassInfo = getTaskClassInfo(task.getClass());

        if (taskClassInfo.incremental) {
            // Add a dummy upToDateWhen spec: this will force TaskOutputs.hasOutputs() to be true.
            task.getOutputs().upToDateWhen(new Spec<Task>() {
                public boolean isSatisfiedBy(Task element) {
                    return true;
                }
            });
        }

        for (Factory<Action<Task>> actionFactory : taskClassInfo.taskActions) {
            task.prependParallelSafeAction(actionFactory.create());
        }

        if (taskClassInfo.validator != null) {
            task.prependParallelSafeAction(taskClassInfo.validator);
            taskClassInfo.validator.addInputsAndOutputs(task);
        }

        return task;
    }

    private TaskClassInfo getTaskClassInfo(Class<? extends Task> type) {
        TaskClassInfo taskClassInfo = classInfos.get(type);
        if (taskClassInfo == null) {
            taskClassInfo = new TaskClassInfo();
            findTaskActions(type, taskClassInfo);

            Validator validator = new Validator();
            validator.attachActions(null, type);

            if (!validator.properties.isEmpty()) {
                taskClassInfo.validator = validator;
            }
            classInfos.put(type, taskClassInfo);
        }
        return taskClassInfo;
    }

    private void findTaskActions(Class<? extends Task> type, TaskClassInfo taskClassInfo) {
        Set<String> methods = new HashSet<String>();
        for (Class current = type; current != null; current = current.getSuperclass()) {
            for (Method method : current.getDeclaredMethods()) {
                attachTaskAction(method, taskClassInfo, methods);
            }
        }
    }

    private void attachTaskAction(final Method method, TaskClassInfo taskClassInfo, Collection<String> processedMethods) {
        if (method.getAnnotation(TaskAction.class) == null) {
            return;
        }
        if (Modifier.isStatic(method.getModifiers())) {
            throw new GradleException(String.format("Cannot use @TaskAction annotation on static method %s.%s().",
                    method.getDeclaringClass().getSimpleName(), method.getName()));
        }
        final Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length > 1) {
            throw new GradleException(String.format(
                    "Cannot use @TaskAction annotation on method %s.%s() as this method takes multiple parameters.",
                    method.getDeclaringClass().getSimpleName(), method.getName()));
        }

        if (parameterTypes.length == 1) {
            if (!parameterTypes[0].equals(IncrementalTaskInputs.class)) {
                throw new GradleException(String.format(
                        "Cannot use @TaskAction annotation on method %s.%s() because %s is not a valid parameter to an action method.",
                        method.getDeclaringClass().getSimpleName(), method.getName(), parameterTypes[0]));
            }
            if (taskClassInfo.incremental) {
                throw new GradleException(String.format("Cannot have multiple @TaskAction methods accepting an %s parameter.", IncrementalTaskInputs.class.getSimpleName()));
            }
            taskClassInfo.incremental = true;
        }
        if (processedMethods.contains(method.getName())) {
            return;
        }
        taskClassInfo.taskActions.add(createActionFactory(method, parameterTypes));
        processedMethods.add(method.getName());
    }

    private Factory<Action<Task>> createActionFactory(final Method method, final Class<?>[] parameterTypes) {
        return new Factory<Action<Task>>() {
            public Action<Task> create() {
                if (parameterTypes.length == 1) {
                    return new IncrementalTaskAction(method);
                } else {
                    return new StandardTaskAction(method);
                }
            }
        };
    }

    private static boolean isGetter(Method method) {
        return ((method.getName().startsWith("get") && method.getReturnType() != Void.TYPE)
                || (method.getName().startsWith("is") && method.getReturnType().equals(boolean.class)))
                && method.getParameterTypes().length == 0 && !Modifier.isStatic(method.getModifiers());
    }

    private static class StandardTaskAction implements Action<Task> {
        private final Method method;

        public StandardTaskAction(Method method) {
            this.method = method;
        }

        public void execute(Task task) {
            ClassLoader original = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(method.getDeclaringClass().getClassLoader());
            try {
                doExecute(task, method.getName());
            } finally {
                Thread.currentThread().setContextClassLoader(original);
            }
        }

        protected void doExecute(Task task, String methodName) {
            JavaReflectionUtil.method(task, Object.class, methodName).invoke(task);
        }
    }

    public static class IncrementalTaskAction extends StandardTaskAction implements ContextAwareTaskAction {

        private TaskArtifactState taskArtifactState;

        public IncrementalTaskAction(Method method) {
            super(method);
        }

        public void contextualise(TaskExecutionContext context) {
            this.taskArtifactState = context == null ? null : context.getTaskArtifactState();
        }

        protected void doExecute(Task task, String methodName) {
            JavaReflectionUtil.method(task, Object.class, methodName, IncrementalTaskInputs.class).invoke(task, taskArtifactState.getInputChanges());
            taskArtifactState = null;
        }
    }

    private static class TaskClassInfo {
        public Validator validator;
        public List<Factory<Action<Task>>> taskActions = new ArrayList<Factory<Action<Task>>>();
        public boolean incremental;
    }

    private class Validator implements Action<Task>, TaskValidator {
        private Set<PropertyInfo> properties = new LinkedHashSet<PropertyInfo>();

        public void addInputsAndOutputs(final TaskInternal task) {
            task.addValidator(this);
            for (final PropertyInfo property : properties) {
                Callable<Object> futureValue = new Callable<Object>() {
                    public Object call() throws Exception {
                        return property.getValue(task).getValue();
                    }
                };

                property.configureAction.update(task, futureValue);
            }
        }

        public void execute(Task task) {
        }

        public void validate(TaskInternal task, Collection<String> messages) {
            List<PropertyValue> propertyValues = new ArrayList<PropertyValue>();
            for (PropertyInfo property : properties) {
                propertyValues.add(property.getValue(task));
            }
            for (PropertyValue propertyValue : propertyValues) {
                propertyValue.checkNotNull(messages);
            }
            for (PropertyValue propertyValue : propertyValues) {
                propertyValue.checkValid(messages);
            }
        }

        public void attachActions(PropertyInfo parent, Class<?> type) {
            Class<?> superclass = type.getSuperclass();
            if (!(superclass == null
                    // Avoid reflecting on classes we know we don't need to look at
                    || superclass.equals(ConventionTask.class) || superclass.equals(DefaultTask.class)
                    || superclass.equals(AbstractTask.class) || superclass.equals(Object.class)
            )) {
                attachActions(parent, superclass);
            }

            Map<String, Field> fields = getFields(type);
            for (Method method : type.getDeclaredMethods()) {
                if (!isGetter(method)) {
                    continue;
                }

                String name = method.getName();
                int prefixLength = name.startsWith("is") ? 2 : 3; // it's 'get' if not 'is'.
                String fieldName = StringUtils.uncapitalize(name.substring(prefixLength));
                String propertyName = fieldName;
                if (parent != null) {
                    propertyName = parent.getName() + '.' + propertyName;
                }
                Field field = fields.get(fieldName);

                PropertyInfo propertyInfo = new PropertyInfo(type, this, parent, propertyName, method, field);

                attachValidationActions(propertyInfo, fieldName, field);

                if (propertyInfo.required) {
                    properties.add(propertyInfo);
                }
            }
        }

        private Map<String, Field> getFields(Class<?> type) {
            Map<String, Field> fields = Maps.newHashMap();
            for (Field field : type.getDeclaredFields()) {
                fields.put(field.getName(), field);
            }
            return fields;
        }

        private void attachValidationActions(PropertyInfo propertyInfo, String fieldName, Field field) {
            final Method method = propertyInfo.method;
            for (PropertyAnnotationHandler handler : handlers) {
                attachValidationAction(handler, propertyInfo, fieldName, method, field);
            }
        }

        private void attachValidationAction(PropertyAnnotationHandler handler, PropertyInfo propertyInfo, String fieldName, Method method, Field field) {
            Class<? extends Annotation> annotationType = handler.getAnnotationType();

            AnnotatedElement annotationTarget = null;
            if (method.getAnnotation(annotationType) != null) {
                annotationTarget = method;
            } else if (field != null && field.getAnnotation(annotationType) != null) {
                annotationTarget = field;
            }
            if (annotationTarget == null) {
                return;
            }

            Annotation optional = annotationTarget.getAnnotation(org.gradle.api.tasks.Optional.class);
            if (optional == null) {
                propertyInfo.setNotNullValidator(notNullValidator);
            }

            propertyInfo.attachActions(handler);
        }
    }

    private interface PropertyValue {
        Object getValue();

        void checkNotNull(Collection<String> messages);

        void checkValid(Collection<String> messages);
    }

    private static class PropertyInfo implements PropertyActionContext {
        private static final ValidationAction NO_OP_VALIDATION_ACTION = new ValidationAction() {
            public void validate(String propertyName, Object value, Collection<String> messages) {
            }
        };
        private static final PropertyValue NO_OP_VALUE = new PropertyValue() {
            public Object getValue() {
                return null;
            }

            public void checkNotNull(Collection<String> messages) {
            }

            public void checkValid(Collection<String> messages) {
            }
        };
        private static final UpdateAction NO_OP_CONFIGURATION_ACTION = new UpdateAction() {
            public void update(TaskInternal task, Callable<Object> futureValue) {
            }
        };

        private final Validator validator;
        private final PropertyInfo parent;
        private final String propertyName;
        private final Method method;
        private ValidationAction validationAction = NO_OP_VALIDATION_ACTION;
        private ValidationAction notNullValidator = NO_OP_VALIDATION_ACTION;
        private UpdateAction configureAction = NO_OP_CONFIGURATION_ACTION;
        public boolean required;
        private final Class<?> type;
        private final Field instanceVariableField;

        private PropertyInfo(Class<?> type, Validator validator, PropertyInfo parent, String propertyName, Method method, Field instanceVariableField) {
            this.type = type;
            this.validator = validator;
            this.parent = parent;
            this.propertyName = propertyName;
            this.method = method;
            this.instanceVariableField = instanceVariableField;
        }

        @Override
        public String toString() {
            return propertyName;
        }

        public String getName() {
            return propertyName;
        }

        public Class<?> getType() {
            return method.getReturnType();
        }

        public Class<?> getInstanceVariableType() {
            return instanceVariableField != null ? instanceVariableField.getType() : null;
        }

        public AnnotatedElement getTarget() {
            return method;
        }

        public void setValidationAction(ValidationAction action) {
            validationAction = action;
        }

        public void setConfigureAction(UpdateAction action) {
            configureAction = action;
        }

        public void setNotNullValidator(ValidationAction notNullValidator) {
            this.notNullValidator = notNullValidator;
        }

        public void attachActions(Class<?> type) {
            validator.attachActions(this, type);
        }

        public PropertyValue getValue(Object rootObject) {
            Object bean = rootObject;
            if (parent != null) {
                PropertyValue parentValue = parent.getValue(rootObject);
                if (parentValue.getValue() == null) {
                    return NO_OP_VALUE;
                }
                bean = parentValue.getValue();
            }

            final Object finalBean = bean;
            final Object value = DeprecationLogger.whileDisabled(new Factory<Object>() {
                public Object create() {
                    return JavaReflectionUtil.method(finalBean, Object.class, method).invoke(finalBean);
                }
            });

            return new PropertyValue() {
                public Object getValue() {
                    return value;
                }

                public void checkNotNull(Collection<String> messages) {
                    notNullValidator.validate(propertyName, value, messages);
                }

                public void checkValid(Collection<String> messages) {
                    if (value != null) {
                        validationAction.validate(propertyName, value, messages);
                    }
                }
            };
        }

        public void attachActions(PropertyAnnotationHandler handler) {
            handler.attachActions(this);
            required = true;
        }
    }
}
