public class Configuration {
    private static final DifferenceListener DUMMY_LISTENER = (difference, context) -> {};

    private static final String DEFAULT_IGNORE_PLACEHOLDER = "${json-unit.ignore}";
    private static final String ALTERNATIVE_IGNORE_PLACEHOLDER = "#{json-unit.ignore}";
    private static final Configuration EMPTY_CONFIGURATION = new Configuration(null, Options.empty(), DEFAULT_IGNORE_PLACEHOLDER, Matchers.empty(), Collections.emptySet(), DUMMY_LISTENER, Collections.emptyList());
    private final BigDecimal tolerance;
    private final Options options;
    private final String ignorePlaceholder;
    private final Matchers matchers;
    private final List<PathOption> pathOptions;
    private final Set<String> pathsToBeIgnored;
    private final DifferenceListener differenceListener;

    @Deprecated
    public Configuration(BigDecimal tolerance, Options options, String ignorePlaceholder) {
        this(tolerance, options, ignorePlaceholder, Matchers.empty(), Collections.emptySet(), DUMMY_LISTENER, Collections.emptyList());
    }

    private Configuration(@Nullable BigDecimal tolerance, Options options, String ignorePlaceholder, Matchers matchers, Set<String> pathsToBeIgnored, DifferenceListener differenceListener, List<PathOption> pathOptions) {
        this.tolerance = tolerance;
        this.options = options;
        this.ignorePlaceholder = ignorePlaceholder;
        this.matchers = matchers;
        this.pathsToBeIgnored = pathsToBeIgnored;
        this.pathOptions = pathOptions;
        this.differenceListener = differenceListener;
    }

    /**
     * Returns an empty configuration.
     */
    @NotNull
    public static Configuration empty() {
        return EMPTY_CONFIGURATION;
    }

    /**
     * Sets numerical comparison tolerance.
     *
     * @param tolerance
     * @return
     */
    @NotNull
    public Configuration withTolerance(@Nullable BigDecimal tolerance) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, pathOptions);
    }

    /**
     * Sets numerical comparison tolerance.
     *
     * @param tolerance
     * @return
     */
    @NotNull
    public Configuration withTolerance(double tolerance) {
        return withTolerance(BigDecimal.valueOf(tolerance));
    }

    /**
     * Adds comparison options.
     *
     * @param first
     * @param next
     * @return
     */
    @NotNull
    public Configuration when(@NotNull Option first, @NotNull Option... next) {
        return withOptions(first, next);
    }

    /**
     * Adds comparison options.
     *
     * @param first
     * @param next
     * @return
     */
    @NotNull
    public Configuration withOptions(@NotNull Option first, @NotNull Option... next) {
        return new Configuration(tolerance, options.with(first, next), ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, pathOptions);
    }

    /**
     * Sets comparison options.
     *
     * @param options
     * @return
     */
    @NotNull
    public Configuration withOptions(@NotNull Options options) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, pathOptions);
    }

    /**
     * Defines general comparison options. See {@link ConfigurationWhen#path} for some examples.
     *
     * @param object an object to apply actions, e.g. {@link ConfigurationWhen#path}, {@link ConfigurationWhen#rootPath}.
     * @param actions actions to be applied on the object.
     *
     * @see ConfigurationWhen#path
     */
    @NotNull
    public final Configuration when(@NotNull PathsParam object, @NotNull ApplicableForPath... actions) {
        Configuration configuration = this;
        for (ApplicableForPath action : actions) {
            configuration = object.apply(configuration, action);
        }
        return configuration;
    }

    @NotNull
    Configuration addPathOption(@NotNull PathOption pathOption) {
        List<PathOption> newOptions = new ArrayList<>(this.pathOptions);
        newOptions.add(pathOption);
        return withPathOptions(newOptions);
    }

    @NotNull
    public Configuration withPathOptions(@NotNull List<PathOption> pathOptions) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, Collections.unmodifiableList(new ArrayList<>(pathOptions)));
    }

    @NotNull
    public Configuration whenIgnoringPaths(@NotNull Collection<String> pathsToBeIgnored) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, Collections.unmodifiableSet(new HashSet<>(pathsToBeIgnored)), differenceListener, pathOptions);
    }

    /**
     * Makes JsonUnit ignore the specified paths in the actual value. If the path matches,
     * it's completely ignored. It may be missing, null or have any value
     *
     * @param pathsToBeIgnored
     * @return
     *
     * @see ConfigurationWhen#thenIgnore
     */
    @NotNull
    public Configuration whenIgnoringPaths(@NotNull String... pathsToBeIgnored) {
        return whenIgnoringPaths(asList(pathsToBeIgnored));
    }

    /**
     * Sets ignore placeholder.
     *
     * @param ignorePlaceholder
     * @return
     */
    @NotNull
    public Configuration withIgnorePlaceholder(@NotNull String ignorePlaceholder) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, pathOptions);
    }

    /**
     * Adds a matcher to be used in ${json-unit.matches:matcherName} macro.
     *
     * @param matcherName
     * @param matcher
     * @return
     */
    @NotNull
    public Configuration withMatcher(@NotNull String matcherName, @NotNull Matcher<?> matcher) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers.with(matcherName, matcher), pathsToBeIgnored, differenceListener, pathOptions);
    }

    /**
     * Sets difference listener
     */
    @NotNull
    public Configuration withDifferenceListener(@NotNull DifferenceListener differenceListener) {
        return new Configuration(tolerance, options, ignorePlaceholder, matchers, pathsToBeIgnored, differenceListener, pathOptions);
    }

    @NotNull
    public static DifferenceListener dummyDifferenceListener() {
        return DUMMY_LISTENER;
    }

    @Nullable
    public Matcher<?> getMatcher(String matcherName) {
        return matchers.getMatcher(matcherName);
    }

    @Nullable
    public BigDecimal getTolerance() {
        return tolerance;
    }

    @NotNull
    public Options getOptions() {
        return options;
    }

    @NotNull
    public String getIgnorePlaceholder() {
        return ignorePlaceholder;
    }

    @NotNull
    public List<PathOption> getPathOptions() {
        return pathOptions;
    }

    @NotNull
    public Set<String> getPathsToBeIgnored() {
        return pathsToBeIgnored;
    }

    @NotNull
    public DifferenceListener getDifferenceListener() {
        return differenceListener;
    }

    public boolean shouldIgnore(String expectedValue) {
        if (DEFAULT_IGNORE_PLACEHOLDER.equals(ignorePlaceholder)) {
            // special handling of default state. We want to support both # and $ before {json-unit.ignore} but do not want to
            // override user specified value if any
            return DEFAULT_IGNORE_PLACEHOLDER.equals(expectedValue) || ALTERNATIVE_IGNORE_PLACEHOLDER.equals(expectedValue);
        } else {
            return ignorePlaceholder.equals(expectedValue);
        }
    }
} public interface ApplicableForPath {
    @NotNull
    Configuration applyForPaths(@NotNull Configuration configuration, @NotNull PathsParam pathsParam);
} public static class PathsParam {
    private final List<String> paths;

    private PathsParam(String path) {
        this.paths = Collections.singletonList(path);
    }

    private PathsParam(String... paths) {
        this.paths = Arrays.asList(paths);
    }

    List<String> getPaths() {
        return paths;
    }

    Configuration apply(Configuration configuration, ApplicableForPath action) {
        return action.applyForPaths(configuration, this);
    }
} public enum Option {
    /**
     * Treats null nodes in actual value as absent. In other words
     * if you expect {"test":{"a":1}} this {"test":{"a":1, "b": null}} will pass the test.
     * When using within {@link ConfigurationWhen#then(Option, Option...)}, exact path to such nodes should be passed,
     * which is test.b in the example above.
     */
    TREATING_NULL_AS_ABSENT,

    /**
     * When comparing arrays, ignores array order. In other words, treats arrays as sets.
     */
    IGNORING_ARRAY_ORDER,

    /**
     * Ignores extra fields in the actual value.
     * When using within {@link ConfigurationWhen#then(Option, Option...)}, path to the nodes with such fields should be passed.
     */
    IGNORING_EXTRA_FIELDS,


    /**
     * Passes even if array in compared document has more items than expected.
     * Items are taken from the beginning of the expected array unless IGNORING_ARRAY_ORDER is specified.
     */
    IGNORING_EXTRA_ARRAY_ITEMS,

    /**
     * Compares only structures. Completely ignores both values and types.
     * Is too lenient, ignores types, prefer {@link Option#IGNORING_VALUES} instead.
     *
     * @deprecated Use IGNORING_VALUES option instead
     */
    @Deprecated
    COMPARING_ONLY_STRUCTURE,

    /**
     * Ignores values but fails if value types are different.
     * When using within {@link ConfigurationWhen#then(Option, Option...)}, path to the node with ignored value should be passed.
     */
    IGNORING_VALUES
} public class Diff {

    // DO NOT remove redundant character escapes, they are needed for Android https://github.com/lukas-krecan/JsonUnit/pull/227
    private static final Pattern ANY_NUMBER_PLACEHOLDER = Pattern.compile("[$#]\\{json-unit.any-number\\}");
    private static final Pattern ANY_BOOLEAN_PLACEHOLDER = Pattern.compile("[$#]\\{json-unit.any-boolean\\}");
    private static final Pattern ANY_STRING_PLACEHOLDER = Pattern.compile("[$#]\\{json-unit.any-string\\}");

    private static final Pattern REGEX_PLACEHOLDER = Pattern.compile("[$#]\\{json-unit.regex\\}(.*)");
    private static final Pattern MATCHER_PLACEHOLDER_PATTERN = Pattern.compile("[$#]\\{json-unit.matches:(.+?)\\}(.*)");

    private static final JsonUnitLogger DEFAULT_DIFF_LOGGER = createLogger("net.javacrumbs.jsonunit.difference.diff");
    private static final JsonUnitLogger DEFAULT_VALUE_LOGGER = createLogger("net.javacrumbs.jsonunit.difference.values");
    static final String DEFAULT_DIFFERENCE_STRING = "expected: <%s> but was: <%s>";

    private final Node expectedRoot;
    private final Node actualRoot;
    private final Differences differences = new Differences();
    private final Path startPath;
    private boolean compared = false;
    private final Configuration configuration;
    private final PathMatcher pathsToBeIgnored;
    private final Map<Option, List<PathOptionMatcher>> specificPathOptions;

    private final JsonUnitLogger diffLogger;
    private final JsonUnitLogger valuesLogger;
    private final String differenceString;

    Diff(Node expected, Node actual, Path startPath, Configuration configuration, JsonUnitLogger diffLogger, JsonUnitLogger valuesLogger, String differenceString) {
        this.expectedRoot = expected;
        this.actualRoot = actual;
        this.startPath = startPath;
        this.configuration = configuration;
        this.diffLogger = diffLogger;
        this.valuesLogger = valuesLogger;
        this.pathsToBeIgnored = PathMatcher.create(configuration.getPathsToBeIgnored());
        this.specificPathOptions = configuration.getPathOptions().stream()
                .flatMap(PathOptionMatcher::createMatchersFromPathOption).collect(Collectors.groupingBy(PathOptionMatcher::getOption));
        this.differenceString = differenceString;
    }

    public static Diff create(Object expected, Object actual, String actualName, String path, Configuration configuration) {
        if (actual instanceof JsonSource) {
            return create(expected, actual, actualName, Path.create(path, ((JsonSource) actual).getPathPrefix()), configuration);
        } else {
            return create(expected, actual, actualName, Path.create(path, ""), configuration);
        }
    }

    public static Diff create(Object expected, Object actual, String actualName, Path path, Configuration configuration) {
        return createInternal(expected, actual,  actualName, path, configuration, DEFAULT_DIFFERENCE_STRING);
    }

    public static Diff createInternal(Object expected, Object actual, String actualName, Path path, Configuration configuration, String differenceString) {
        return new Diff(convertToJson(quoteIfNeeded(expected), "expected", true), convertToJson(actual, actualName, false), path, configuration, DEFAULT_DIFF_LOGGER, DEFAULT_VALUE_LOGGER, differenceString);
    }

    private void compare() {
        if (!compared) {
            Node part = startPath.getNode(actualRoot);
            Context context = new Context(expectedRoot, part, startPath, startPath, configuration);
            if (part.isMissingNode()) {
                structureDifferenceFound(context, "Missing node in path \"%s\".", startPath);
            } else {
                compareNodes(context);
            }
            compared = true;

            logDifferences();
        }
    }

    /**
     * Compares object nodes.
     */
    private void compareObjectNodes(Context context) {
        Node expected = context.getExpectedNode();
        Node actual = context.getActualNode();

        Path path = context.getActualPath();

        Map<String, Node> expectedFields = getFields(expected);
        Map<String, Node> actualFields = getFields(actual);

        Set<String> expectedKeys = expectedFields.keySet();
        Set<String> actualKeys = actualFields.keySet();

        if (!expectedKeys.equals(actualKeys)) {
            Set<String> missingKeys = getMissingKeys(expectedKeys, actualKeys);
            Set<String> extraKeys = removeNullExtraKeysWhereNeeded(actual,
                    getExtraKeys(context.getActualPath(), expectedKeys, actualKeys),
                    context.getActualPath());

            removePathsToBeIgnored(path, extraKeys);
            removePathsToBeIgnored(path, missingKeys);

            removeMissingIgnoredElements(expected, missingKeys);

            if (!missingKeys.isEmpty() || !extraKeys.isEmpty()) {
                for (String key : missingKeys) {
                    reportDifference(DifferenceImpl.missing(context.inField(key)));
                }
                for (String key : extraKeys) {
                    reportDifference(DifferenceImpl.extra(context.inField(key)));
                }
                String missingKeysMessage = getMissingKeysMessage(missingKeys, path);
                String extraKeysMessage = getExtraKeysMessage(extraKeys, path);
                structureDifferenceFound(context, "Different keys found in node \"%s\"%s%s, " + differenceString(), path, missingKeysMessage, extraKeysMessage, normalize(expected), normalize(actual));
            }
        }

        for (String fieldName : commonFields(expectedFields, actualFields)) {
            compareNodes(context.inField(fieldName));
        }
    }

    private void removeMissingIgnoredElements(Node expected, Set<String> missingKeys) {
        missingKeys.removeIf(missingKey -> shouldIgnoreElement(expected.get(missingKey)));
    }

    @SuppressWarnings("unchecked")
    private String normalize(Node node) {
        Map<String, Object> map = (Map<String, Object>) node.getValue();
        return prettyPrint(new TreeMap<>(map));
    }

    private void reportDifference(Difference difference) {
        configuration.getDifferenceListener().diff(difference,
                differenceContext(configuration, actualRoot, expectedRoot));
    }

    private void removePathsToBeIgnored(Path path, Set<String> extraKeys) {
        if (!configuration.getPathsToBeIgnored().isEmpty()) {
            extraKeys.removeIf(key -> shouldIgnorePath(path.toField(key)));
        }
    }

    /**
     * Removes extra keys that are null and should be treated as absent.
     */
    private Set<String> removeNullExtraKeysWhereNeeded(Node actual, Set<String> extraKeys, Path actualPath) {
        Set<String> notNullExtraKeys = new TreeSet<>();
        for (String extraKey : extraKeys) {
            if (!hasOption(actualPath.toField(extraKey), TREATING_NULL_AS_ABSENT) || !actual.get(extraKey).isNull()) {
                notNullExtraKeys.add(extraKey);
            }
        }
        return notNullExtraKeys;
    }


    private static String getMissingKeysMessage(Set<String> missingKeys, Path path) {
        if (!missingKeys.isEmpty()) {
            return ", missing: " + appendKeysToPrefix(missingKeys, path);
        } else {
            return "";
        }
    }

    private static Set<String> getMissingKeys(Set<String> expectedKeys, Collection<String> actualKeys) {
        Set<String> missingKeys = new TreeSet<>(expectedKeys);
        missingKeys.removeAll(actualKeys);
        return missingKeys;
    }

    private static String getExtraKeysMessage(Set<String> extraKeys, Path path) {
        if (!extraKeys.isEmpty()) {
            return ", extra: " + appendKeysToPrefix(extraKeys, path);
        } else {
            return "";
        }
    }

    private Set<String> getExtraKeys(Path path, Set<String> expectedKeys, Collection<String> actualKeys) {
        if (!hasOption(path, IGNORING_EXTRA_FIELDS)) {
            Set<String> extraKeys = new TreeSet<>(actualKeys);
            extraKeys.removeAll(expectedKeys);
            return extraKeys;
        } else {
            return emptySet();
        }
    }

    private boolean hasOption(Path path, Option option) {
        boolean hasOption = configuration.getOptions().contains(option);
        if (specificPathOptions.containsKey(option)) {
            for (PathOptionMatcher matcher : specificPathOptions.get(option)) {
                if (matcher.matches(path.getFullPath())) {
                    hasOption = matcher.isAdded();
                }
            }
        }
        return hasOption;
    }

    private static String appendKeysToPrefix(Iterable<String> keys, Path prefix) {
        Iterator<String> iterator = keys.iterator();
        StringBuilder buffer = new StringBuilder();
        while (iterator.hasNext()) {
            String key = iterator.next();
            buffer.append("\"").append(prefix.toField(key)).append("\"");
            if (iterator.hasNext()) {
                buffer.append(",");
            }
        }
        return buffer.toString();
    }

    /**
     * Compares two nodes.
     */
    private void compareNodes(Context context) {
        if (shouldIgnorePath(context.getActualPath())) {
            return;
        }

        Node expectedNode = context.getExpectedNode();
        Node actualNode = context.getActualNode();

        NodeType expectedNodeType = expectedNode.getNodeType();
        NodeType actualNodeType = actualNode.getNodeType();

        Path fieldPath = context.getActualPath();

        //ignoring value
        if (expectedNodeType == NodeType.STRING && configuration.shouldIgnore(expectedNode.asText())) {
            return;
        }

        if (shouldIgnoreElement(expectedNode)) {
            return;
        }

        // Any number
        if (checkAny(NodeType.NUMBER, ANY_NUMBER_PLACEHOLDER, "a number", context)) {
            return;
        }

        // Any boolean
        if (checkAny(NodeType.BOOLEAN, ANY_BOOLEAN_PLACEHOLDER, "a boolean", context)) {
            return;
        }

        // Any string
        if (checkAny(NodeType.STRING, ANY_STRING_PLACEHOLDER, "a string", context)) {
            return;
        }

        if (checkMatcher(context)) {
            return;
        }

        if (!expectedNodeType.equals(actualNodeType)) {
            reportValueDifference(context, "Different value found in node \"%s\", " + differenceString() + ".", fieldPath, quoteTextValue(expectedNode), quoteTextValue(actualNode));
        } else {
            switch (expectedNodeType) {
                case OBJECT:
                    compareObjectNodes(context);
                    break;
                case ARRAY:
                    compareArrayNodes(context);
                    break;
                case STRING:
                    compareStringValues(context);
                    break;
                case NUMBER:
                    BigDecimal actualValue = actualNode.decimalValue();
                    BigDecimal expectedValue = expectedNode.decimalValue();
                    if (configuration.getTolerance() != null && !hasOption(context.getActualPath(), IGNORING_VALUES)) {
                        BigDecimal diff = expectedValue.subtract(actualValue).abs();
                        if (diff.compareTo(configuration.getTolerance()) > 0) {
                            List<Object> arguments = new ArrayList<>(Arrays.asList(fieldPath, quoteTextValue(expectedValue), quoteTextValue(actualValue), diff.toString()));
                            String message = "Different value found in node \"%s\", " + differenceString() + ", difference is %s";
                            if (configuration.getTolerance().compareTo(BigDecimal.ZERO) != 0) {
                                arguments.add(configuration.getTolerance());
                                message += ", tolerance is %s";
                            }
                            reportValueDifference(context, message, arguments.toArray());
                        }
                    } else {
                        compareValues(context, expectedValue, actualValue);
                    }
                    break;
                case BOOLEAN:
                    compareValues(context, expectedNode.asBoolean(), actualNode.asBoolean());
                    break;
                case NULL:
                    //nothing
                    break;
                default:
                    throw new IllegalStateException("Unexpected node type " + expectedNodeType);
            }
        }
    }

    private boolean shouldIgnoreElement(Node expectedNode) {
        return expectedNode.getNodeType() == NodeType.STRING && "${json-unit.ignore-element}".equals(expectedNode.asText());
    }

    private boolean shouldIgnorePath(Path fieldPath) {
        return pathsToBeIgnored.matches(fieldPath.getFullPath());
    }

    private boolean checkMatcher(Context context) {
        Node expectedNode = context.getExpectedNode();
        Node actualNode = context.getActualNode();

        if (expectedNode.getNodeType() == NodeType.STRING) {
            Matcher patternMatcher = MATCHER_PLACEHOLDER_PATTERN.matcher(expectedNode.asText());
            if (patternMatcher.matches()) {
                String matcherName = patternMatcher.group(1);
                new HamcrestHandler(configuration, this::reportValueDifference, this::structureDifferenceFound)
                    .matchHamcrestMatcher(context, actualNode, patternMatcher, matcherName);
                return true;
            }
        }
        return false;
    }

    private boolean checkAny(NodeType type, Pattern placeholder, String name, Context context) {
        Node expectedNode = context.getExpectedNode();
        Node actualNode = context.getActualNode();

        if (expectedNode.getNodeType() == NodeType.STRING && placeholder.matcher(expectedNode.asText()).matches()) {
            if (actualNode.getNodeType() == type) {
                return true;
            } else {
                reportValueDifference(context, "Different value found in node \"%s\", " + differenceString() + ".", context.getActualPath(), name, quoteTextValue(actualNode));
                return true;
            }
        }
        return false;
    }

    private void compareStringValues(Context context) {
        String expectedValue = context.getExpectedNode().asText();
        String actualValue = context.getActualNode().asText();
        Path path = context.getActualPath();

        if (hasOption(context.getActualPath(), IGNORING_VALUES)) {
            return;
        }
        Matcher regexpMatcher = REGEX_PLACEHOLDER.matcher(expectedValue);
        if (regexpMatcher.matches()) {
            String pattern = regexpMatcher.group(1);
            if (!actualValue.matches(pattern)) {
                reportValueDifference(context, "Different value found in node \"%s\". Pattern %s did not match %s.", path, quoteTextValue(pattern), quoteTextValue(actualValue));
            }
        } else {
            compareValues(context, expectedValue, actualValue);
        }
    }


    private void compareValues(Context context, Object expectedValue, Object actualValue) {
        if (!hasOption(context.getActualPath(), IGNORING_VALUES)) {
            if (!expectedValue.equals(actualValue)) {
                reportValueDifference(context, "Different value found in node \"%s\", " + differenceString() + ".", context.getActualPath(), quoteTextValue(expectedValue), quoteTextValue(actualValue));
            }
        }
    }

    /**
     * IntelliJ is looking for this string to show comparison view.
     */
    private String differenceString() {
        return differenceString;
    }

    /**
     * If the value is String than it's quoted in ".
     *
     * @param value
     * @return
     */
    public static Object quoteTextValue(Object value) {
        if (value instanceof String) {
            return "\"" + value + "\"";
        } else {
            return value;
        }
    }


    private void compareArrayNodes(Context context) {
        Node expectedNode = context.getExpectedNode();
        Node actualNode = context.getActualNode();
        Path path = context.getActualPath();

        List<Node> expectedElements = asList(expectedNode.arrayElements());
        List<Node> actualElements = asList(actualNode.arrayElements());


        if (failOnExtraArrayItems(context.getActualPath())) {
            if (expectedElements.size() != actualElements.size()) {
                structureDifferenceFound(context.length(expectedElements.size()), "Array \"%s\" has different length, expected: <%d> but was: <%d>.", path, expectedElements.size(), actualElements.size());
            }
        } else {
            // if we expect more elements in the array than we get, it's error even when IGNORING_EXTRA_ARRAY_ITEMS
            if (expectedElements.size() > actualElements.size()) {
                structureDifferenceFound(context.length("at least " + expectedElements.size()), "Array \"%s\" has invalid length, expected: <at least %d> but was: <%d>.", path, expectedElements.size(), actualElements.size());
            }
        }

        if (hasOption(context.getActualPath(), IGNORING_ARRAY_ORDER)) {
            ComparisonResult arrayComparison = compareArraysIgnoringOrder(expectedElements, actualElements, path);
            List<NodeWithIndex> missingValues = arrayComparison.getMissingValues();
            List<NodeWithIndex> extraValues = arrayComparison.getExtraValues();
            if (expectedElements.size() == actualElements.size() && missingValues.size() == 1 && extraValues.size() == 1) {
                // handling special case where only one difference is found.
                NodeWithIndex missing = missingValues.get(0);
                NodeWithIndex extra = extraValues.get(0);

                Path expectedPath = context.getExpectedPath().toElement(missing.getIndex());
                Path actualPath = context.getActualPath().toElement(extra.getIndex());
                valueDifferenceFound(context, "Different value found when comparing expected array element %s to actual element %s.", expectedPath, actualPath);
                compareNodes(new Context(missing.getNode(), extra.getNode(), expectedPath, actualPath, configuration));
            } else if (failOnExtraArrayItems(context.getActualPath()) && (!missingValues.isEmpty() || !extraValues.isEmpty())) {
                reportMissingValues(context, missingValues);
                reportExtraValues(context, extraValues);

                valueDifferenceFound(context, "Array \"%s\" has different content. Missing values: %s, extra values: %s, expected: <%s> but was: <%s>", path, missingValues, extraValues, expectedNode, actualNode);
            } else if (!missingValues.isEmpty()) {
                reportMissingValues(context, missingValues);
                valueDifferenceFound(context, "Array \"%s\" has different content. Missing values: %s, expected: <%s> but was: <%s>", path, missingValues, expectedNode, actualNode);
            }
        } else {
            if (expectedElements.size() > actualElements.size()) {
                for (int i = actualElements.size(); i < expectedElements.size(); i++) {
                    reportDifference(DifferenceImpl.missing(context.missingElement(i)));
                }
                valueDifferenceFound(context, "Array \"%s\" has different content. Missing values: %s, expected: <%s> but was: <%s>", path, expectedElements.subList(actualElements.size(), expectedElements.size()), expectedNode, actualNode);
            } else if (failOnExtraArrayItems(context.getActualPath()) && expectedElements.size() < actualElements.size()) {
                for (int i = expectedElements.size(); i < actualElements.size(); i++) {
                    reportDifference(DifferenceImpl.extra(context.extraElement(i)));
                }
                valueDifferenceFound(context, "Array \"%s\" has different content. Extra values: %s, expected: <%s> but was: <%s>", path, actualElements.subList(expectedElements.size(), actualElements.size()), expectedNode, actualNode);
            }
            for (int i = 0; i < Math.min(expectedElements.size(), actualElements.size()); i++) {
                compareNodes(context.toElement(i));
            }
        }
    }

    private void reportMissingValues(Context context, List<NodeWithIndex> missingValues) {
        for (NodeWithIndex missingValue : missingValues) {
            reportDifference(DifferenceImpl.missing(context.missingElement(missingValue.getIndex())));
        }
    }

    private void reportExtraValues(Context context, List<NodeWithIndex> extraValues) {
        for (NodeWithIndex extraValue : extraValues) {
            reportDifference(DifferenceImpl.extra(context.extraElement(extraValue.getIndex())));
        }
    }

    private ComparisonResult compareArraysIgnoringOrder(List<Node> expectedElements, List<Node> actualElements, Path path) {
        return new ArrayComparison(expectedElements, actualElements, path, configuration).compareArraysIgnoringOrder();
    }


    private boolean failOnExtraArrayItems(Path path) {
        return !hasOption(path, IGNORING_EXTRA_ARRAY_ITEMS);
    }


    private List<Node> asList(Iterator<Node> elements) {
        List<Node> result = new ArrayList<>();
        while (elements.hasNext()) {
            Node node = elements.next();
            result.add(node);
        }
        return Collections.unmodifiableList(result);
    }


    private void structureDifferenceFound(Context context, String message, Object... arguments) {
        differences.add(new JsonDifference(context, message, arguments));
    }

    @SuppressWarnings("deprecation")
    private void valueDifferenceFound(Context context, String message, Object... arguments) {
        if (!hasOption(context.getActualPath(), COMPARING_ONLY_STRUCTURE)) {
            differences.add(new JsonDifference(context, message, arguments));
        }
    }

    private void reportValueDifference(Context context, String message, Object... arguments) {
        reportDifference(DifferenceImpl.different(context));
        valueDifferenceFound(context, message, arguments);
    }

    private Set<String> commonFields(Map<String, Node> expectedFields, Map<String, Node> actualFields) {
        Set<String> result = new TreeSet<>(expectedFields.keySet());
        result.retainAll(actualFields.keySet());
        return Collections.unmodifiableSet(result);
    }


    public boolean similar() {
        compare();
        return differences.isEmpty();
    }

    private void logDifferences() {
        if (!differences.isEmpty()) {
            if (diffLogger.isEnabled()) {
                diffLogger.log(getDifferences().trim());
            }
            if (valuesLogger.isEnabled()) {
                valuesLogger.log("Comparing expected:\n{}\n------------\nwith actual:\n{}\n", expectedRoot, startPath.getNode(actualRoot));
            }
        }
    }

    /**
     * Returns children of an ObjectNode.
     *
     * @param node
     * @return
     */
    private static Map<String, Node> getFields(Node node) {
        Map<String, Node> result = new HashMap<>();
        Iterator<KeyValue> fields = node.fields();
        while (fields.hasNext()) {
            KeyValue field = fields.next();
            result.put(field.getKey(), field.getValue());
        }
        return Collections.unmodifiableMap(result);
    }

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

    public String differences() {
        if (similar()) {
            return "JSON documents have the same value.";
        }
        return getDifferences();
    }

    private String getDifferences() {
        return formatDifferences("", differences);
    }


    public void failIfDifferent(){
        failIfDifferent(null);
    }

    public void failIfDifferent(String message) {
        if (!similar()) {
            throw createException(message, differences);
        }
    }

    private static JsonUnitLogger createLogger(String name) {
        if (isClassPresent("org.slf4j.Logger")) {
            return new JsonUnitLogger.SLF4JLogger(name);
        } else {
            return NULL_LOGGER;
        }
    }
} public interface JsonSource {
    Object getJson();

    String getPathPrefix();

    List<String> getMatchingPaths();
} public interface Node {

    enum NodeType implements ValueExtractor {
        OBJECT("object") {
            @Override
            public Object getValue(Node node) {
                // custom conversion to map. We want be consistent and native mapping may have different rules for
                // serializing numbers, dates etc.
                return new JsonMap(node);
            }
        },
        ARRAY("array") {
            @Override
            public Object getValue(Node node) {
                return new JsonList(node);
            }
        },
        STRING("string") {
            @Override
            public Object getValue(Node node) {
                return node.asText();
            }
        },
        NUMBER("number") {
            @Override
            public Object getValue(Node node) {
                return node.decimalValue();
            }
        },
        BOOLEAN("boolean") {
            @Override
            public Object getValue(Node node) {
                return node.asBoolean();
            }
        },
        NULL("null") {
            @Override
            public Object getValue(Node node) {
                return null;
            }
        };

        private final String description;

        NodeType(String description) {
            this.description = description;
        }

        public String getDescription() {
            return description;
        }
    }

    Node element(int index);

    Iterator<KeyValue> fields();

    Node get(String key);

    boolean isMissingNode();

    boolean isNull();

    Iterator<Node> arrayElements();

    /**
     * Array length
     */
    int size();

    String asText();

    NodeType getNodeType();

    BigDecimal decimalValue();

    /**
     * Returns true if the value is an integer. 1 is an integer 1.0, 1.1, 1e3, 1e0, 1e-3 is not.
     */
    default boolean isIntegralNumber() {
        BigDecimal decimalValue = decimalValue();
        String text = decimalValue.toString();
        return decimalValue.scale() == 0 && !text.contains("e") && !text.contains("E");
    }

    Boolean asBoolean();

    Object getValue();

    void ___do_not_implement_this_interface_seriously();

    class KeyValue {
        private final String key;
        private final Node value;

        KeyValue(String key, Node value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public Node getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "{\"" + key + "\": " + value + "}";
        }
    }

    Node MISSING_NODE = new Node() {
        @Override
        public Node element(int index) {
            return MISSING_NODE;
        }

        @Override
        public Iterator<KeyValue> fields() {
            return Collections.emptyIterator();
        }

        @Override
        public Node get(String key) {
            return this;
        }

        @Override
        public boolean isMissingNode() {
            return true;
        }

        @Override
        public boolean isNull() {
            return false;
        }

        @Override
        public Iterator<Node> arrayElements() {
            return Collections.emptyIterator();
        }

        @Override
        public int size() {
            return 0;
        }

        @Override
        public String asText() {
            throw new UnsupportedOperationException();
        }

        @Override
        public NodeType getNodeType() {
            throw new UnsupportedOperationException();
        }

        @Override
        public BigDecimal decimalValue() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Boolean asBoolean() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Object getValue() {
            return null;
        }

        @Override
        public void ___do_not_implement_this_interface_seriously() {}

        @Override
        public String toString() {
            return "<missing>";
        }
    };
    interface ValueExtractor {
        Object getValue(Node node);
    }

    class JsonMap extends LinkedHashMap<String, Object> implements NodeWrapper {
        private final Node wrappedNode;

        JsonMap(Node node) {
            wrappedNode = node;
            Iterator<KeyValue> fields = node.fields();
            while (fields.hasNext()) {
                KeyValue keyValue = fields.next();
                put(keyValue.getKey(), keyValue.getValue().getValue());
            }
        }

        @Override
        public String toString() {
            return prettyPrint(this);
        }

        @Override
        public Node getWrappedNode() {
            return wrappedNode;
        }
    }

    class JsonList extends LinkedList<Object> implements NodeWrapper {
        private final Node wrappedNode;

        JsonList(Node node) {
            Iterator<Node> nodeIterator = node.arrayElements();
            while (nodeIterator.hasNext()) {
                Node arrayNode = nodeIterator.next();
                add(arrayNode.getValue());
            }
            wrappedNode = node;
        }

        @Override
        public Node getWrappedNode() {
            return wrappedNode;
        }
    }
} public final class Path {
    private final String path;
    private final String pathPrefix;

    private static final Pattern arrayPattern = Pattern.compile("(.*)\\[(-?\\d+)]");

    /**
     * We need to ignore "\." when splitting path.
     */
    private static final Pattern dotWithPreviousChar = Pattern.compile("[^\\\\]\\.");

    private Path(String path, String pathPrefix) {
        this.path = path;
        this.pathPrefix = pathPrefix;
    }

    public static Path root() {
          return create("", "");
      }

    public static Path create(String path) {
        return create(path, "");
    }

    public static Path create(String path, String pathPrefix) {
        return new Path(path, pathPrefix);
    }

    public Path copy(String newPath) {
        return new Path(newPath, pathPrefix);
    }

    public Path asPrefix() {
        return new Path("", getFullPath());
    }

    String getPath() {
        return path;
    }

    String getFullPath() {
        if (pathPrefix.isEmpty()) {
            return path;
        } else if (path.startsWith("[")) {
            return pathPrefix + path;
        } else if (!path.isEmpty()){
            return pathPrefix + "." + path;
        } else {
            return pathPrefix;
        }
    }

    /**
     * Construct path to a filed.
     */
    Path toField(String name) {
        if (isRoot()) {
            return copy(name);
        } else {
            return copy(path + "." + name);
        }
    }

    /**
     * Constructs path to an array element.
     */
    Path toElement(int i) {
        return copy(path + "[" + i + "]");
    }

    /**
     * Constructs path to an array element length.
     */
    Path length() {
        return toField("length()");
    }

    public Path to(String name) {
        if (name.startsWith("[")){
            return copy(path + name);
        } else {
            return toField(name);
        }
    }

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

    /**
     * Returns node with given path.
     */
    Node getNode(Node root) {
        if (isRoot()) {
            return root;
        }

        Node startNode = root;
        Matcher pathMatcher = dotWithPreviousChar.matcher(path);
        int pos = 0;
        while (pathMatcher.find()) {
            String step = path.substring(pos, pathMatcher.end() - 1);
            pos = pathMatcher.end();
            startNode = doStep(step, startNode);
        }
        startNode = doStep(path.substring(pos), startNode);
        return startNode;
    }

    private boolean isRoot() {
        return path.length() == 0;
    }

    private static Node doStep(String step, Node startNode) {
        // this regexp translates to \\\. which is find one backspace following by a dot
        step = step.replaceAll("\\\\\\.", ".");
        Matcher matcher = arrayPattern.matcher(step);
        if (!matcher.matches()) {
            startNode = startNode.get(step);
        } else {
            if (matcher.group(1).length() != 0) {
                startNode = startNode.get(matcher.group(1));
            }

            int index = Integer.parseInt(matcher.group(2));
            if (index < 0) {
                startNode = startNode.element(startNode.size() + index);
            } else {
                startNode = startNode.element(index);
            }
        }
        return startNode;
    }
} public interface DifferenceListener {
    void diff(Difference difference, DifferenceContext context);
} public static Diff create(Object expected, Object actual, String actualName, String path, Configuration configuration) {
    if (actual instanceof JsonSource) {
        return create(expected, actual, actualName, Path.create(path, ((JsonSource) actual).getPathPrefix()), configuration);
    } else {
        return create(expected, actual, actualName, Path.create(path, ""), configuration);
    }
} public static Object quoteTextValue(Object value) {
    if (value instanceof String) {
        return "\"" + value + "\"";
    } else {
        return value;
    }
} public static Node getNode(Object root, String path) {
    return getNode(root, Path.create(path));
} public static boolean nodeAbsent(Object json, String path, Configuration configuration) {
    return nodeAbsent(json, Path.create(path), configuration);
}
import net.javacrumbs.jsonunit.core.Configuration;
import net.javacrumbs.jsonunit.core.ConfigurationWhen.ApplicableForPath;
import net.javacrumbs.jsonunit.core.ConfigurationWhen.PathsParam;
import net.javacrumbs.jsonunit.core.Option;
import net.javacrumbs.jsonunit.core.internal.Diff;
import net.javacrumbs.jsonunit.core.internal.JsonSource;
import net.javacrumbs.jsonunit.core.internal.Node;
import net.javacrumbs.jsonunit.core.internal.Path;
import net.javacrumbs.jsonunit.core.listener.DifferenceListener;
import org.hamcrest.Matcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static java.util.Collections.singletonList;
import static net.javacrumbs.jsonunit.core.Option.COMPARING_ONLY_STRUCTURE;
import static net.javacrumbs.jsonunit.core.internal.Diff.create;
import static net.javacrumbs.jsonunit.core.internal.Diff.quoteTextValue;
import static net.javacrumbs.jsonunit.core.internal.JsonUtils.getNode;
import static net.javacrumbs.jsonunit.core.internal.JsonUtils.nodeAbsent;
import static net.javacrumbs.jsonunit.core.internal.Node.NodeType.ARRAY;
import static net.javacrumbs.jsonunit.core.internal.Node.NodeType.NULL;
import static net.javacrumbs.jsonunit.core.internal.Node.NodeType.NUMBER;
import static net.javacrumbs.jsonunit.core.internal.Node.NodeType.OBJECT;
import static net.javacrumbs.jsonunit.core.internal.Node.NodeType.STRING;
import static org.hamcrest.MatcherAssert.assertThat;
/**
 * Copyright 2009-2019 the original author or authors.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 net.javacrumbs.jsonunit.core.internal.matchers;





/**
 * Internal class, please do not use outside the library
 */
public final class InternalMatcher {
    public static final String ACTUAL = "actual";

    private final Path path;
    private final Object actual;
    private final String description;
    private final Configuration configuration;


    public InternalMatcher(@Nullable Object actual, @NotNull Path path, @NotNull String description, @NotNull Configuration configuration) {
        this.path = path;
        this.actual = actual;
        this.description = description;
        this.configuration = configuration;
    }

    private InternalMatcher(@NotNull Object actual, @NotNull String pathPrefix) {
        this(actual, Path.create("", pathPrefix), "", Configuration.empty());
    }

    @NotNull
    public InternalMatcher whenIgnoringPaths(@NotNull String... pathsToBeIgnored) {
        return new InternalMatcher(actual, path, description, configuration.whenIgnoringPaths(pathsToBeIgnored));
    }

    /**
     * Sets the description of this object.
     */
    @NotNull
    public InternalMatcher describedAs(@NotNull String description) {
        return new InternalMatcher(actual, path, description, configuration);
    }

    /**
     * Sets the placeholder that can be used to ignore values.
     * The default value is ${json-unit.ignore}
     */
    @NotNull
    public InternalMatcher withIgnorePlaceholder(@NotNull String ignorePlaceholder) {
        return new InternalMatcher(actual, path, description, configuration.withIgnorePlaceholder(ignorePlaceholder));
    }

    /**
     * Sets the tolerance for floating number comparison. If set to null, requires exact match of the values.
     * For example, if set to 0.01, ignores all differences lower than 0.01, so 1 and 0.9999 are considered equal.
     */
    @NotNull
    public InternalMatcher withTolerance(double tolerance) {
        return withTolerance(BigDecimal.valueOf(tolerance));
    }

    /**
     * Sets the tolerance for floating number comparison. If set to null, requires exact match of the values.
     * For example, if set to 0.01, ignores all differences lower than 0.01, so 1 and 0.9999 are considered equal.
     */
    @NotNull
    public InternalMatcher withTolerance(@Nullable BigDecimal tolerance) {
        return new InternalMatcher(actual, path, description, configuration.withTolerance(tolerance));
    }


    /**
     * Adds a internalMatcher to be used in ${json-unit.matches:matcherName} macro.
     */
    @NotNull
    public InternalMatcher withMatcher(@NotNull String matcherName, @NotNull Matcher<?> matcher) {
        return new InternalMatcher(actual, path, description, configuration.withMatcher(matcherName, matcher));
    }

    @NotNull
    public InternalMatcher withDifferenceListener(@NotNull DifferenceListener differenceListener) {
        return new InternalMatcher(actual, path, description, configuration.withDifferenceListener(differenceListener));
    }

    /**
     * Sets options changing comparison behavior. This method has to be called
     * <b>before</b> assertion.
     * For more info see {@link net.javacrumbs.jsonunit.core.Option}
     *
     * @param firstOption
     * @param otherOptions
     * @see net.javacrumbs.jsonunit.core.Option
     */
    @NotNull
    public InternalMatcher withOptions(@NotNull Option firstOption, @NotNull Option... otherOptions) {
        return new InternalMatcher(actual, path, description, configuration.withOptions(firstOption, otherOptions));
    }

    /**
     * Sets advanced/local options. This method has to be called <b>before</b> assertion.
     * For more info see {@link Configuration#when(PathsParam, ApplicableForPath...)}
     *
     * @param object
     * @param actions
     * @see Configuration#when(PathsParam, ApplicableForPath...)
     */
    @NotNull
    public final InternalMatcher when(@NotNull PathsParam object, @NotNull ApplicableForPath... actions) {
        return new InternalMatcher(actual, path, description, configuration.when(object, actions));
    }


    public void isEqualTo(@Nullable Object expected) {
        Diff diff = createDiff(expected, configuration);
        diff.failIfDifferent(description);
    }


    /**
     * Fails if the selected JSON is not a String or is not present or the value
     * is not equal to expected value.
     */
    public void isStringEqualTo(@Nullable String expected) {
        isString();
        Node node = getActualNode();
        if (!node.asText().equals(expected)) {
            failOnDifference(quoteTextValue(expected), quoteTextValue(node.asText()));