/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * 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.android.class2nonsdklist;

import com.google.common.base.Strings;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class ApiResolver {
    private final List<ApiComponents> mPotentialPublicAlternatives;
    private final Set<PackageAndClassName> mPublicApiClasses;

    private static final Pattern LINK_TAG_PATTERN = Pattern.compile("\\{@link ([^\\}]+)\\}");
    private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code ([^\\}]+)\\}");
    private static final Integer MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES = 29;

    public ApiResolver() {
        mPotentialPublicAlternatives = null;
        mPublicApiClasses = null;
    }

    public ApiResolver(Set<String> publicApis) {
        mPotentialPublicAlternatives = publicApis.stream()
                .map(api -> {
                    try {
                        return ApiComponents.fromDexSignature(api);
                    } catch (SignatureSyntaxError e) {
                        throw new RuntimeException("Could not parse public API signature:", e);
                    }
                })
                .collect(Collectors.toList());
        mPublicApiClasses = mPotentialPublicAlternatives.stream()
                .map(api -> api.getPackageAndClassName())
                .collect(Collectors.toCollection(HashSet::new));
    }

    /**
     * Verify that all public alternatives are valid.
     *
     * @param publicAlternativesString String containing public alternative explanations.
     * @param signature                Signature of the member that has the annotation.
     */
    public void resolvePublicAlternatives(String publicAlternativesString, String signature,
                                          Integer maxSdkVersion)
            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
                    RequiredAlternativeNotSpecifiedError, MultipleAlternativesFoundWarning {
        if (Strings.isNullOrEmpty(publicAlternativesString) && maxSdkVersion != null
                && maxSdkVersion >= MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES) {
            throw new RequiredAlternativeNotSpecifiedError();
        }
        if (publicAlternativesString != null && mPotentialPublicAlternatives != null) {
            // Grab all instances of type {@link foo}
            Matcher matcher = LINK_TAG_PATTERN.matcher(publicAlternativesString);
            boolean hasLinkAlternative = false;
            // Validate all link tags
            while (matcher.find()) {
                hasLinkAlternative = true;
                String alternativeString = matcher.group(1);
                ApiComponents alternative = ApiComponents.fromLinkTag(alternativeString,
                        signature);
                if (alternative.getMemberName().isEmpty()) {
                    // Provided class as alternative
                    if (!mPublicApiClasses.contains(alternative.getPackageAndClassName())) {
                        throw new ClassAlternativeNotFoundError(alternative);
                    }
                } else if (!mPotentialPublicAlternatives.contains(alternative)) {
                    // If the link is not a public alternative, it must because the link does not
                    // contain the method parameter types, e.g. {@link foo.bar.Baz#foo} instead of
                    // {@link foo.bar.Baz#foo(int)}. If the method name is unique within the class,
                    // we can handle it.
                    if (!Strings.isNullOrEmpty(alternative.getMethodParameterTypes())) {
                        throw new MemberAlternativeNotFoundError(alternative);
                    }
                    List<ApiComponents> almostMatches = mPotentialPublicAlternatives.stream()
                            .filter(api -> api.equalsIgnoringParam(alternative))
                            .collect(Collectors.toList());
                    if (almostMatches.size() == 0) {
                        throw new MemberAlternativeNotFoundError(alternative);
                    } else if (almostMatches.size() > 1) {
                        throw new MultipleAlternativesFoundWarning(alternative, almostMatches);
                    }
                }
            }
            // No {@link ...} alternatives exist; try looking for {@code ...}
            if (!hasLinkAlternative) {
                if (!CODE_TAG_PATTERN.matcher(publicAlternativesString).find()) {
                    throw new NoAlternativesSpecifiedError();
                }
            }
        }
    }
}
