package ru.yandex.autotests.directapi.steps.forecast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import ru.yandex.autotests.directapi.common.api45mng.WordstatReportInfo;
import ru.yandex.autotests.irt.testutils.TestUtilsException;
import ru.yandex.autotests.irt.testutils.beandiffer.diff.Diff;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.BeanDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.DefaultArrayDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.DefaultCollectionDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.Differ;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.EnumDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.MapDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.MatcherDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.differ.SimpleTypeDiffer;
import ru.yandex.autotests.irt.testutils.beandiffer.matchvariation.MatchVariation;

import static ru.yandex.autotests.directapi.steps.forecast.ApproximateCompareHelper.areApproximatelyEqual;

public class ApproximateNumbersMatchVariation implements MatchVariation {
    private Map<String, Differ> differMap;
    private boolean ignoreExceptionsWhenGetFieldsValue;
    private boolean isIgnoreNullFieldsOfExpectedBean;
    protected final int percents;
    protected final int itemsPercents;
    protected final boolean compareAlsoFields;

    public ApproximateNumbersMatchVariation(int percents, int itemsPercents, boolean compareAlsoFields) {
        this.percents = percents;
        this.differMap = new HashMap<>();
        this.ignoreExceptionsWhenGetFieldsValue = false;
        this.isIgnoreNullFieldsOfExpectedBean = false;
        this.itemsPercents = itemsPercents;
        this.compareAlsoFields = compareAlsoFields;
    }

    @Override
    public UseDiffer forFields(String... fieldPaths) {
        return new DefaultUseDiffer(this, Arrays.asList(fieldPaths));
    }

    @Override
    public UseDiffer forClasses(Class... types) {
        List<String> typesName = new ArrayList<>();
        for (Class type : types) {
            typesName.add(type.getCanonicalName());
        }
        return new ApproximateNumbersMatchVariation.DefaultUseDiffer(this, typesName);
    }

    public class DefaultUseDiffer implements UseDiffer {
        private ApproximateNumbersMatchVariation matchVariation;
        private List<String> keys;


        public DefaultUseDiffer(ApproximateNumbersMatchVariation matchVariation, List<String> keys) {
            this.matchVariation = matchVariation;
            this.keys = keys;
        }

        @Override
        public MatchVariation use(Differ differ) {
            for (String key : keys) {
                matchVariation.differMap.put(key, differ);
            }
            return matchVariation;
        }

        @Override
        public MatchVariation useMatcher(Matcher matcher) {
            for (String key : keys) {
                matchVariation.differMap.put(key, new MatcherDiffer(matcher));
            }
            return matchVariation;
        }

    }

    @Override
    public Differ getDiffer(Class type) {
        return getDiffer("", type);
    }

    @Override
    public Differ getDiffer(String fieldPath, Class type) {
        Differ differ = null;
        if (fieldPath != null && differMap.containsKey(fieldPath)) {
            differ = differMap.get(fieldPath);
        } else if (type != null && differMap.containsKey(type.getCanonicalName())) {
            differ = differMap.get(type.getCanonicalName());
        } else {
            Differ[] differs = new Differ[]{
                    new ApproximateWordstatReportInfoTypeDiffer(percents, itemsPercents, compareAlsoFields),
                    new ApproximateNumbersTypeDiffer(percents),
                    new SimpleTypeDiffer(),
                    new EnumDiffer(),
                    new DefaultArrayDiffer(),
                    new DefaultCollectionDiffer(),
                    new MapDiffer(),
                    new BeanDiffer()
            };

            for (Differ curDiffer : differs) {
                if (curDiffer.accepts(type)) {
                    differ = curDiffer;
                    break;
                }
            }
        }

        if (differ == null) {
            throw new TestUtilsException("Не найден диффер для указанного типа: " + type.getCanonicalName());
        }

        differ.withFieldPath(fieldPath)
                .withMatchVariation(this);
        return differ;
    }

    @Override
    public boolean isIgnoreExceptionsWhenGetFieldsValue() {
        return ignoreExceptionsWhenGetFieldsValue;
    }

    @Override
    public MatchVariation setIgnoreExceptionsWhenGetFieldsValue(boolean value) {
        this.ignoreExceptionsWhenGetFieldsValue = value;
        return this;
    }

    @Override
    public boolean isIgnoreNullFieldsOfExpectedBean() {
        return isIgnoreNullFieldsOfExpectedBean;
    }

    @Override
    public MatchVariation setIgnoreNullFieldsOfExpectedBean(boolean value) {
        this.isIgnoreNullFieldsOfExpectedBean = value;
        return this;
    }

    public static class ApproximateNumbersTypeDiffer<T> extends Differ<T> {

        private final int percents;

        public ApproximateNumbersTypeDiffer(int percents) {
            this.percents = percents;
        }

        @Override
        public boolean accepts(final Class type) {
            return Number.class.isAssignableFrom(type);
        }

        @Override
        public Diff compare(T expected, T actual) {
            Differ differ = new MatcherDiffer<>(new IsNumbersApproximatelyEqual(expected, percents))
                    .withFieldPath(fieldPath);
            return differ.compare(expected, actual);
        }

    }

    public static class ApproximateWordstatReportInfoTypeDiffer extends Differ<WordstatReportInfo> {
        private final int percents;
        private final int itemsPercents;
        private final boolean compareAlsoFields;

        public ApproximateWordstatReportInfoTypeDiffer(int percents, int itemsPercents, boolean compareAlsoFields) {
            this.percents = percents;
            this.itemsPercents = itemsPercents;
            this.compareAlsoFields = compareAlsoFields;
        }

        @Override
        public boolean accepts(final Class type) {
            return WordstatReportInfo.class.isAssignableFrom(type);
        }

        @Override
        public Diff compare(WordstatReportInfo expected, WordstatReportInfo actual) {
            Differ<WordstatReportInfo> differ = new MatcherDiffer<WordstatReportInfo>(
                    new ApproximateWordstatReportMatcher(expected, percents, itemsPercents, compareAlsoFields))
                    .withFieldPath(fieldPath);
            return differ.compare(expected, actual);
        }

    }

    public static class IsNumbersApproximatelyEqual<T> extends BaseMatcher<T> {
        private final Object expectedValue;
        private final int percents;

        public IsNumbersApproximatelyEqual(T equalArg, int percents) {
            this.expectedValue = equalArg;
            this.percents = percents;
        }

        public boolean matches(Object actualValue) {
            return areApproximatelyEqual(actualValue, this.expectedValue, percents);
        }

        public void describeTo(Description description) {
            description.appendValue(this.expectedValue);
        }
    }
}
