/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

struct AccessibilityTextHelpers
{
    /*  Wraps a CharPtr into a stdlib-compatible iterator.

        MSVC's std::reverse_iterator requires the wrapped iterator to be default constructible
        when building in C++20 mode, but I don't really want to add public default constructors to
        the CharPtr types. Instead, we add a very basic default constructor here which sets the
        wrapped CharPtr to nullptr.
    */
    template <typename CharPtr>
    class CharPtrIteratorAdapter
    {
    public:
        using difference_type = int;
        using value_type = decltype (*std::declval<CharPtr>());
        using pointer = value_type*;
        using reference = value_type;
        using iterator_category = std::bidirectional_iterator_tag;

        CharPtrIteratorAdapter() = default;
        constexpr explicit CharPtrIteratorAdapter (CharPtr arg) : ptr (arg) {}

        constexpr auto operator*() const { return *ptr; }

        constexpr CharPtrIteratorAdapter& operator++()
        {
            ++ptr;
            return *this;
        }

        constexpr CharPtrIteratorAdapter& operator--()
        {
            --ptr;
            return *this;
        }

        constexpr bool operator== (const CharPtrIteratorAdapter& other) const { return ptr == other.ptr; }
        constexpr bool operator!= (const CharPtrIteratorAdapter& other) const { return ptr != other.ptr; }

        constexpr auto operator+ (difference_type offset) const { return CharPtrIteratorAdapter { ptr + offset }; }
        constexpr auto operator- (difference_type offset) const { return CharPtrIteratorAdapter { ptr - offset }; }

    private:
        CharPtr ptr { {} };
    };

    template <typename CharPtr>
    static auto makeCharPtrIteratorAdapter (CharPtr ptr)
    {
        return CharPtrIteratorAdapter<CharPtr> { ptr };
    }

    enum class BoundaryType
    {
        character,
        word,
        line,
        document
    };

    enum class Direction
    {
        forwards,
        backwards
    };

    enum class ExtendSelection
    {
        no,
        yes
    };

    /*  Indicates whether a function may return the current text position, in the case that the
        position already falls on a text unit boundary.
    */
    enum class IncludeThisBoundary
    {
        no,     //< Always search for the following boundary, even if the current position falls on a boundary
        yes     //< Return the current position if it falls on a boundary
    };

    /*  Indicates whether a word boundary should include any whitespaces that follow the
        non-whitespace characters.
    */
    enum class IncludeWhitespaceAfterWords
    {
        no,     //< The word ends on the first whitespace character
        yes     //< The word ends after the last whitespace character
    };

    /*  Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't
        require the iterators to have any member type aliases.
    */
    template <typename Iter>
    static int countDifference (Iter from, Iter to)
    {
        int distance = 0;

        while (from != to)
        {
            ++from;
            ++distance;
        }

        return distance;
    }

    /*  Returns the number of characters between ptr and the next word end in a specific
        direction.

        If ptr is inside a word, the result will be the distance to the end of the same
        word.
    */
    template <typename CharPtr>
    static int findNextWordEndOffset (CharPtr beginIn,
                                      CharPtr endIn,
                                      CharPtr ptrIn,
                                      Direction direction,
                                      IncludeThisBoundary includeBoundary,
                                      IncludeWhitespaceAfterWords includeWhitespace)
    {
        const auto begin = makeCharPtrIteratorAdapter (beginIn);
        const auto end   = makeCharPtrIteratorAdapter (endIn);
        const auto ptr   = makeCharPtrIteratorAdapter (ptrIn);

        const auto move = [&] (auto b, auto e, auto iter)
        {
            const auto isSpace = [] (juce_wchar c) { return CharacterFunctions::isWhitespace (c); };

            const auto start = [&]
            {
                if (iter == b && includeBoundary == IncludeThisBoundary::yes)
                    return b;

                const auto nudged = iter - (iter != b && includeBoundary == IncludeThisBoundary::yes ? 1 : 0);

                return includeWhitespace == IncludeWhitespaceAfterWords::yes
                       ? std::find_if (nudged, e, isSpace)
                       : std::find_if_not (nudged, e, isSpace);
            }();

            const auto found = includeWhitespace == IncludeWhitespaceAfterWords::yes
                             ? std::find_if_not (start, e, isSpace)
                             : std::find_if (start, e, isSpace);

            return countDifference (iter, found);
        };

        return direction == Direction::forwards ? move (begin, end, ptr)
                                                : -move (std::make_reverse_iterator (end),
                                                         std::make_reverse_iterator (begin),
                                                         std::make_reverse_iterator (ptr));
    }

    /*  Returns the number of characters between ptr and the beginning of the next line in a
        specific direction.
    */
    template <typename CharPtr>
    static int findNextLineOffset (CharPtr beginIn,
                                   CharPtr endIn,
                                   CharPtr ptrIn,
                                   Direction direction,
                                   IncludeThisBoundary includeBoundary)
    {
        const auto begin = makeCharPtrIteratorAdapter (beginIn);
        const auto end   = makeCharPtrIteratorAdapter (endIn);
        const auto ptr   = makeCharPtrIteratorAdapter (ptrIn);

        const auto findNewline = [] (auto from, auto to) { return std::find (from, to, juce_wchar { '\n' }); };

        if (direction == Direction::forwards)
        {
            if (ptr != begin && includeBoundary == IncludeThisBoundary::yes && *(ptr - 1) == '\n')
                return 0;

            const auto newline = findNewline (ptr, end);
            return countDifference (ptr, newline) + (newline == end ? 0 : 1);
        }

        const auto rbegin   = std::make_reverse_iterator (ptr);
        const auto rend     = std::make_reverse_iterator (begin);

        return -countDifference (rbegin, findNewline (rbegin + (rbegin == rend || includeBoundary == IncludeThisBoundary::yes ? 0 : 1), rend));
    }

    /*  Unfortunately, the method of computing end-points of text units depends on context, and on
        the current platform.

        Some examples of different behaviour:
        - On Android, updating the cursor/selection always searches for the next text unit boundary;
          but on Windows, ExpandToEnclosingUnit() should not move the starting point of the
          selection if it already at a unit boundary. This means that we need both inclusive and
          exclusive methods for finding the next text boundary.
        - On Android, moving the cursor by 'words' should move to the first space following a
          non-space character in the requested direction. On Windows, a 'word' includes trailing
          whitespace, but not preceding whitespace. This means that we need a way of specifying
          whether whitespace should be included when navigating by words.
    */
    static int findTextBoundary (const AccessibilityTextInterface& textInterface,
                                 int currentPosition,
                                 BoundaryType boundary,
                                 Direction direction,
                                 IncludeThisBoundary includeBoundary,
                                 IncludeWhitespaceAfterWords includeWhitespace)
    {
        const auto numCharacters = textInterface.getTotalNumCharacters();
        const auto isForwards = (direction == Direction::forwards);
        const auto currentClamped = jlimit (0, numCharacters, currentPosition);

        switch (boundary)
        {
            case BoundaryType::character:
            {
                const auto offset = includeBoundary == IncludeThisBoundary::yes ? 0
                                                                                : (isForwards ? 1 : -1);
                return jlimit (0, numCharacters, currentPosition + offset);
            }

            case BoundaryType::word:
            {
                const auto str = textInterface.getText ({ 0, numCharacters });
                return currentClamped + findNextWordEndOffset (str.begin(),
                                                               str.end(),
                                                               str.begin() + currentClamped,
                                                               direction,
                                                               includeBoundary,
                                                               includeWhitespace);
            }

            case BoundaryType::line:
            {
                const auto str = textInterface.getText ({ 0, numCharacters });
                return currentClamped + findNextLineOffset (str.begin(),
                                                            str.end(),
                                                            str.begin() + currentClamped,
                                                            direction,
                                                            includeBoundary);
            }

            case BoundaryType::document:
                return isForwards ? numCharacters : 0;
        }

        jassertfalse;
        return -1;
    }

    /*  Adjusts the current text selection range, using an algorithm appropriate for cursor movement
        on Android.
    */
    static Range<int> findNewSelectionRangeAndroid (const AccessibilityTextInterface& textInterface,
                                                    BoundaryType boundaryType,
                                                    ExtendSelection extend,
                                                    Direction direction)
    {
        const auto oldPos = textInterface.getTextInsertionOffset();
        const auto cursorPos = findTextBoundary (textInterface,
                                                 oldPos,
                                                 boundaryType,
                                                 direction,
                                                 IncludeThisBoundary::no,
                                                 IncludeWhitespaceAfterWords::no);

        if (extend == ExtendSelection::no)
            return { cursorPos, cursorPos };

        const auto currentSelection = textInterface.getSelection();
        const auto start = currentSelection.getStart();
        const auto end   = currentSelection.getEnd();
        return Range<int>::between (cursorPos, oldPos == start ? end : start);
    }
};

} // namespace juce
