/*
 * Copyright (C) 2016 Stuart Gilbert
 *
 * 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 ie.stu.papercut.compiler;

import com.github.zafarkhaja.semver.Version;
import com.google.auto.service.AutoService;
import ie.stu.papercut.Milestone;
import ie.stu.papercut.Refactor;
import ie.stu.papercut.RemoveThis;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@SupportedAnnotationTypes({
        "ie.stu.papercut.RemoveThis",
        "ie.stu.papercut.Refactor",
        "ie.stu.papercut.Milestone"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions({
        "versionCode",
        "versionName"
})
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
    private static final String OPTION_VERSION_CODE = "versionCode";
    private static final String OPTION_VERSION_NAME = "versionName";
    private static final Set<String> milestones = new HashSet<>();

    private Messager messager;
    private String versionCode;
    private String versionName;

    @Override
    public synchronized void init(final ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        versionCode = processingEnv.getOptions().get(OPTION_VERSION_CODE);
        versionName = processingEnv.getOptions().get(OPTION_VERSION_NAME);

        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        buildMilestoneList(roundEnv.getElementsAnnotatedWith(Milestone.class));

        parseTechDebtElements(roundEnv.getElementsAnnotatedWith(RemoveThis.class));
        parseTechDebtElements(roundEnv.getElementsAnnotatedWith(Refactor.class));

        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

    private void buildMilestoneList(Set<? extends Element> elements) {
        for (final Element element : elements) {
            final Milestone milestone = element.getAnnotation(Milestone.class);
            final String milestoneName = milestone.value();

            milestones.add(milestoneName);
        }
    }

    private void parseTechDebtElements(Set<? extends Element> elements) {
        for (final Element element : elements) {
            final RemoveThis removeThisAnnotation = element.getAnnotation(RemoveThis.class);
            final Refactor refactorAnnotation = element.getAnnotation(Refactor.class);

            // Handling the case where both annotations are present would be overly complicated. Since they're
            // essentially interchangeable I doubt anyone will encounter this. If you're reading this because you
            // encountered the issue then please pick between the annotations. The docs should help you choose.
            if (removeThisAnnotation != null && refactorAnnotation != null) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Specifying @RemoveThis and @Refactor on the same" +
                        "code is not currently supported.", element);
            }

            final String description;
            final String givenDate;
            final boolean stopShip;
            final String milestone;
            final String versionCode;
            final String versionName;
            final String annotationType;

            if (removeThisAnnotation != null) {
                description = removeThisAnnotation.value();
                givenDate = removeThisAnnotation.date();
                stopShip = removeThisAnnotation.stopShip();
                milestone = removeThisAnnotation.milestone();
                versionCode = removeThisAnnotation.versionCode();
                versionName = removeThisAnnotation.versionName();
                annotationType = RemoveThis.class.getSimpleName();
            } else {
                description = refactorAnnotation.value();
                givenDate = refactorAnnotation.date();
                stopShip = refactorAnnotation.stopShip();
                milestone = refactorAnnotation.milestone();
                versionCode = refactorAnnotation.versionCode();
                versionName = refactorAnnotation.versionName();
                annotationType = Refactor.class.getSimpleName();
            }

            final Diagnostic.Kind messageKind = stopShip ? Diagnostic.Kind.ERROR : Diagnostic.Kind.WARNING;
            final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

            Date date = null;

            try {
                if (!givenDate.isEmpty()) {
                    date = simpleDateFormat.parse(givenDate);
                }
            } catch (final ParseException e) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Incorrect date format in Papercut annotation." +
                        "Please use YYYY-MM-DD format.");
            }

            boolean breakConditionMet = noConditionsSet(date, milestone, versionCode, versionName);

            breakConditionMet = breakConditionMet || dateConditionMet(date);
            breakConditionMet = breakConditionMet || milestoneConditionMet(milestone);
            breakConditionMet = breakConditionMet || versionCodeConditionMet(versionCode);
            breakConditionMet = breakConditionMet || versionNameConditionMet(versionName, element);

            if (breakConditionMet) {
                if (!description.isEmpty()) {
                    messager.printMessage(messageKind, String.format("@%1$s found with description %2$s at: ",
                            annotationType, description), element);
                } else {
                    messager.printMessage(messageKind, String.format("@%1$s found at: ", annotationType), element);
                }
            }
        }
    }

    private boolean noConditionsSet(final Date date, final String milestone, final String versionCode,
                                    final String versionName) {
        return date == null && milestone.isEmpty() && versionCode.isEmpty() && versionName.isEmpty();
    }

    private boolean dateConditionMet(final Date date) {
        return date != null && (date.before(new Date()) || date.equals(new Date()));
    }

    private boolean milestoneConditionMet(final String milestone) {
        return !milestone.isEmpty() && !milestones.contains(milestone);
    }

    private boolean versionCodeConditionMet(final String versionCode) {
        return !versionCode.isEmpty() && Integer.parseInt(versionCode) <= Integer.parseInt(this.versionCode);
    }

    private boolean versionNameConditionMet(final String versionName, final Element element) {
        // Drop out quickly if there's no versionName set otherwise the try/catch below is doomed to fail.
        if (versionName.isEmpty()) return false;

        int comparison;

        try {
            final Version conditionVersion = Version.valueOf(versionName);
            final Version currentVersion = Version.valueOf(this.versionName);

            comparison = Version.BUILD_AWARE_ORDER.compare(conditionVersion, currentVersion);
        } catch (final IllegalArgumentException | com.github.zafarkhaja.semver.ParseException e) {
            messager.printMessage(Diagnostic.Kind.ERROR, String.format("Failed to parse versionName: %1$s. " +
                    "Please use a versionName that matches the specification on http://semver.org/", versionName),
                    element);

            // Assume the break condition is met if the versionName is invalid.
            return true;
        }

        return !versionName.isEmpty() && comparison <= 0;
    }
}