/*
 * Copyright 2010-2015 JetBrains s.r.o.
 *
 * 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.jetbrains.kotlin.js.patterns;

import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.idea.KotlinLanguage;
import org.jetbrains.kotlin.js.descriptorUtils.DescriptorUtilsKt;
import org.jetbrains.kotlin.name.Name;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.OverridingUtil;

import java.util.Arrays;
import java.util.List;

public final class PatternBuilder {

    @NotNull
    private static final NamePredicate KOTLIN_NAME_PREDICATE = new NamePredicate("kotlin");

    @NotNull
    private static final Name KOTLIN_NAME = Name.identifier(KotlinLanguage.NAME.toLowerCase());

    private PatternBuilder() {
    }

    @NotNull
    public static DescriptorPredicate pattern(@NotNull NamePredicate checker, @NotNull String stringWithPattern) {
        List<NamePredicate> checkers = Lists.newArrayList(checker);
        checkers.addAll(parseFqNamesFromString(stringWithPattern));
        return pattern(checkers, parseArgumentsFromString(stringWithPattern));
    }

    @NotNull
    public static DescriptorPredicate pattern(@NotNull String stringWithPattern, @NotNull NamePredicate checker) {
        List<NamePredicate> checkers = Lists.newArrayList(parseFqNamesFromString(stringWithPattern));
        checkers.add(checker);
        return pattern(checkers);
    }

    @NotNull
    public static DescriptorPredicate pattern(@NotNull String stringWithPattern) {
        return pattern(parseFqNamesFromString(stringWithPattern), parseArgumentsFromString(stringWithPattern));
    }

    @NotNull
    private static List<NamePredicate> parseFqNamesFromString(@NotNull String stringWithPattern) {
        stringWithPattern = getNamePatternFromString(stringWithPattern);
        String[] subPatterns = stringWithPattern.split("\\.");
        List<NamePredicate> checkers = Lists.newArrayList();
        for (String subPattern : subPatterns) {
            String[] validNames = subPattern.split("\\|");
            checkers.add(new NamePredicate(validNames));
        }
        return checkers;
    }

    @Nullable
    private static List<NamePredicate> parseArgumentsFromString(@NotNull String stringWithPattern) {
        stringWithPattern = getArgumentsPatternFromString(stringWithPattern);
        if (stringWithPattern == null) return null;

        List<NamePredicate> checkers = Lists.newArrayList();
        if (stringWithPattern.isEmpty()) {
            return checkers;
        }

        String[] subPatterns = stringWithPattern.split("\\,");
        for (String subPattern : subPatterns) {
            String[] validNames = subPattern.split("\\|");
            checkers.add(new NamePredicate(validNames));
        }
        return checkers;
    }

    @NotNull
    private static String getNamePatternFromString(@NotNull String stringWithPattern) {
        int left = stringWithPattern.indexOf("(");
        if (left < 0) {
            return stringWithPattern;
        }
        else {
            return stringWithPattern.substring(0, left);
        }
    }

    @Nullable
    private static String getArgumentsPatternFromString(@NotNull String stringWithPattern) {
        int left = stringWithPattern.indexOf("(");
        if (left < 0) {
            return null;
        }
        else {
            int right = stringWithPattern.indexOf(")");
            assert right == stringWithPattern.length() - 1 : "expected ')' at the end: " + stringWithPattern;
            return stringWithPattern.substring(left + 1, right);
        }
    }

    @NotNull
    private static DescriptorPredicate pattern(@NotNull List<NamePredicate> checkers) {
        return pattern(checkers, null);
    }

    @NotNull
    private static DescriptorPredicate pattern(@NotNull List<NamePredicate> checkers, @Nullable List<NamePredicate> arguments) {
        assert !checkers.isEmpty();
        List<NamePredicate> checkersWithPrefixChecker = Lists.newArrayList();
        if (!checkers.get(0).test(KOTLIN_NAME)) {
            checkersWithPrefixChecker.add(KOTLIN_NAME_PREDICATE);
        }

        checkersWithPrefixChecker.addAll(checkers);

        assert checkersWithPrefixChecker.size() > 1;

        List<NamePredicate> argumentCheckers = arguments != null ? Lists.newArrayList(arguments) : null;

        return new DescriptorPredicate() {
            @Override
            public boolean test(FunctionDescriptor descriptor) {
                //TODO: no need to wrap if we check beforehand
                try {
                    return doApply(descriptor);
                }
                catch (IllegalArgumentException e) {
                    return false;
                }
            }

            private boolean doApply(@NotNull FunctionDescriptor descriptor) {
                List<Name> nameParts = DescriptorUtils.getFqName(descriptor).pathSegments();
                if (nameParts.size() != checkersWithPrefixChecker.size()) return false;

                return allNamePartsValid(nameParts) && checkAllArgumentsValidIfNeeded(descriptor);
            }

            private boolean checkAllArgumentsValidIfNeeded(@NotNull FunctionDescriptor descriptor) {
                if (argumentCheckers != null) {
                    List<ValueParameterDescriptor> valueParameterDescriptors = descriptor.getValueParameters();
                    if (valueParameterDescriptors.size() != argumentCheckers.size()) {
                        return false;
                    }
                    for (int i = 0; i < valueParameterDescriptors.size(); i++) {
                        ValueParameterDescriptor valueParameterDescriptor = valueParameterDescriptors.get(i);
                        Name name = DescriptorUtilsKt.getNameIfStandardType(valueParameterDescriptor.getType());
                        NamePredicate namePredicate = argumentCheckers.get(i);
                        if (!namePredicate.test(name)) return false;
                    }
                }
                return true;
            }

            private boolean allNamePartsValid(@NotNull List<Name> nameParts) {
                for (int i = 0; i < nameParts.size(); ++i) {
                    Name namePart = nameParts.get(i);
                    NamePredicate correspondingPredicate = checkersWithPrefixChecker.get(i);
                    if (!correspondingPredicate.test(namePart)) {
                        return false;
                    }
                }
                return true;
            }
        };
    }

    @NotNull
    public static DescriptorPredicate pattern(@NotNull NamePredicate... checkers) {
        return pattern(Arrays.asList(checkers));
    }

    @NotNull
    public static DescriptorPredicateImpl pattern(@NotNull String... names) {
        return new DescriptorPredicateImpl(names);
    }

    public static class DescriptorPredicateImpl implements DescriptorPredicate {
        private final String[] names;

        private String receiverFqName;

        private boolean checkOverridden;

        public DescriptorPredicateImpl(String... names) {
            this.names = names;
        }

        public DescriptorPredicateImpl isExtensionOf(String receiverFqName) {
            this.receiverFqName = receiverFqName;
            return this;
        }

        public DescriptorPredicateImpl checkOverridden() {
            this.checkOverridden = true;
            return this;
        }

        private boolean matches(@NotNull CallableDescriptor callable) {
            DeclarationDescriptor descriptor = callable;
            int nameIndex = names.length - 1;
            while (true) {
                if (nameIndex == -1) {
                    return false;
                }

                if (!descriptor.getName().asString().equals(names[nameIndex])) {
                    return false;
                }

                nameIndex--;
                descriptor = descriptor.getContainingDeclaration();
                if (descriptor instanceof PackageFragmentDescriptor) {
                    return nameIndex == 0 && names[0].equals(((PackageFragmentDescriptor) descriptor).getFqName().asString());
                }
            }
        }

        @Override
        public boolean test(FunctionDescriptor functionDescriptor) {
            ReceiverParameterDescriptor actualReceiver = functionDescriptor.getExtensionReceiverParameter();
            if (actualReceiver != null) {
                if (receiverFqName == null) return false;

                String actualReceiverFqName = DescriptorUtilsKt.getKotlinTypeFqName(actualReceiver.getType(), false);

                if (!actualReceiverFqName.equals(receiverFqName)) return false;
            }

            if (!(functionDescriptor.getContainingDeclaration() instanceof ClassDescriptor)) {
                return matches(functionDescriptor);
            }

            for (CallableMemberDescriptor real : OverridingUtil.getOverriddenDeclarations(functionDescriptor)) {
                if (matches(real)) {
                    return true;
                }
            }

            if (checkOverridden) {
                for (CallableDescriptor overridden : DescriptorUtils.getAllOverriddenDescriptors(functionDescriptor)) {
                    if (matches(overridden)) {
                        return true;
                    }
                }
            }

            return false;
        }
    }
}
