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

   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
{

template <typename Ptr>
struct CharPtrIteratorTraits
{
    using difference_type = int;
    using value_type = decltype (*std::declval<Ptr>());
    using pointer = value_type*;
    using reference = value_type;
    using iterator_category = std::bidirectional_iterator_tag;
};

} // namespace juce

namespace std
{

template <> struct iterator_traits<juce::CharPointer_UTF8>  : juce::CharPtrIteratorTraits<juce::CharPointer_UTF8>  {};
template <> struct iterator_traits<juce::CharPointer_UTF16> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF16> {};
template <> struct iterator_traits<juce::CharPointer_UTF32> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF32> {};
template <> struct iterator_traits<juce::CharPointer_ASCII> : juce::CharPtrIteratorTraits<juce::CharPointer_ASCII> {};

} // namespace std

namespace juce
{

struct AccessibilityTextHelpers
{
    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 begin,
                                      CharPtr end,
                                      CharPtr ptr,
                                      Direction direction,
                                      IncludeThisBoundary includeBoundary,
                                      IncludeWhitespaceAfterWords includeWhitespace)
    {
        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 begin,
                                   CharPtr end,
                                   CharPtr ptr,
                                   Direction direction,
                                   IncludeThisBoundary includeBoundary)
    {
        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
