using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;

namespace Avalonia.Media
{
    /// <summary>
    ///     Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
    /// </summary>
    public sealed class GlyphRun : IDisposable
    {
        private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
        private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();

        private IGlyphRunImpl? _glyphRunImpl;
        private GlyphTypeface _glyphTypeface;
        private double _fontRenderingEmSize;
        private int _biDiLevel;
        private Point? _baselineOrigin;
        private GlyphRunMetrics? _glyphRunMetrics;

        private ReadOnlySlice<char> _characters;

        private IReadOnlyList<ushort> _glyphIndices;
        private IReadOnlyList<double>? _glyphAdvances;
        private IReadOnlyList<Vector>? _glyphOffsets;
        private IReadOnlyList<int>? _glyphClusters;

        private int _offsetToFirstCharacter;

        /// <summary>
        ///     Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
        /// </summary>
        /// <param name="glyphTypeface">The glyph typeface.</param>
        /// <param name="fontRenderingEmSize">The rendering em size.</param>
        /// <param name="glyphIndices">The glyph indices.</param>
        /// <param name="glyphAdvances">The glyph advances.</param>
        /// <param name="glyphOffsets">The glyph offsets.</param>
        /// <param name="characters">The characters.</param>
        /// <param name="glyphClusters">The glyph clusters.</param>
        /// <param name="biDiLevel">The bidi level.</param>
        public GlyphRun(
            GlyphTypeface glyphTypeface,
            double fontRenderingEmSize,
            ReadOnlySlice<char> characters,
            IReadOnlyList<ushort> glyphIndices,
            IReadOnlyList<double>? glyphAdvances = null,
            IReadOnlyList<Vector>? glyphOffsets = null,
            IReadOnlyList<int>? glyphClusters = null,
            int biDiLevel = 0)
        {
            _glyphTypeface = glyphTypeface;

            FontRenderingEmSize = fontRenderingEmSize;

            Characters = characters;

            _glyphIndices = glyphIndices;

            GlyphAdvances = glyphAdvances;

            GlyphOffsets = glyphOffsets;

            GlyphClusters = glyphClusters;

            BiDiLevel = biDiLevel;
        }

        /// <summary>
        ///     Gets the <see cref="Media.GlyphTypeface"/> for the <see cref="GlyphRun"/>.
        /// </summary>
        public GlyphTypeface GlyphTypeface => _glyphTypeface;

        /// <summary>
        ///     Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
        /// </summary>
        public double FontRenderingEmSize
        {
            get => _fontRenderingEmSize;
            set => Set(ref _fontRenderingEmSize, value);
        }

        /// <summary>
        ///     Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
        /// </summary>
        public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);

        /// <summary>
        /// 
        /// </summary>
        public GlyphRunMetrics Metrics
        {
            get
            {
                _glyphRunMetrics ??= CreateGlyphRunMetrics();

                return _glyphRunMetrics.Value;
            }
        }

        /// <summary>
        ///     Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
        /// </summary>
        public Point BaselineOrigin
        {
            get
            {
                _baselineOrigin ??= CalculateBaselineOrigin();

                return _baselineOrigin.Value;
            }
            set => Set(ref _baselineOrigin, value);
        }

        /// <summary>
        ///     Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
        /// </summary>
        public IReadOnlyList<ushort> GlyphIndices
        {
            get => _glyphIndices;
            set => Set(ref _glyphIndices, value);
        }

        /// <summary>
        ///     Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
        /// </summary>
        public IReadOnlyList<double>? GlyphAdvances
        {
            get => _glyphAdvances;
            set => Set(ref _glyphAdvances, value);
        }

        /// <summary>
        ///     Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
        /// </summary>
        public IReadOnlyList<Vector>? GlyphOffsets
        {
            get => _glyphOffsets;
            set => Set(ref _glyphOffsets, value);
        }

        /// <summary>
        ///     Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
        /// </summary>
        public ReadOnlySlice<char> Characters
        {
            get => _characters;
            set => Set(ref _characters, value);
        }

        /// <summary>
        ///     Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
        /// </summary>
        public IReadOnlyList<int>? GlyphClusters
        {
            get => _glyphClusters;
            set => Set(ref _glyphClusters, value);
        }

        /// <summary>
        ///     Gets or sets the bidirectional nesting level of the <see cref="GlyphRun"/>.
        /// </summary>
        public int BiDiLevel
        {
            get => _biDiLevel;
            set => Set(ref _biDiLevel, value);
        }

        /// <summary>
        /// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
        /// </summary>
        internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;

        /// <summary>
        /// Returns <c>true</c> if the text direction is left-to-right. Otherwise, returns <c>false</c>.
        /// </summary>
        public bool IsLeftToRight => ((BiDiLevel & 1) == 0);

        /// <summary>
        /// The platform implementation of the <see cref="GlyphRun"/>.
        /// </summary>
        public IGlyphRunImpl GlyphRunImpl
        {
            get
            {
                if (_glyphRunImpl == null)
                {
                    Initialize();
                }

                return _glyphRunImpl!;
            }
        }

        /// <summary>
        /// Retrieves the offset from the leading edge of the <see cref="GlyphRun"/>
        /// to the leading or trailing edge of a caret stop containing the specified character hit.
        /// </summary>
        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the offset.</param>
        /// <returns>
        /// A <see cref="double"/> that represents the offset from the leading edge of the <see cref="GlyphRun"/>
        /// to the leading or trailing edge of a caret stop containing the character hit.
        /// </returns>
        public double GetDistanceFromCharacterHit(CharacterHit characterHit)
        {
            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;

            var distance = 0.0;

            if (IsLeftToRight)
            {
                if (GlyphClusters != null)
                {
                    if (characterIndex < GlyphClusters[0])
                    {
                        return 0;
                    }

                    if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
                    {
                        return Metrics.WidthIncludingTrailingWhitespace;
                    }
                }

                var glyphIndex = FindGlyphIndex(characterIndex);

                if (GlyphClusters != null)
                {
                    var currentCluster = GlyphClusters[glyphIndex];

                    //Move to the end of the glyph cluster
                    if (characterHit.TrailingLength > 0)
                    {
                        while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
                        {
                            glyphIndex++;
                        }
                    }
                }

                for (var i = 0; i < glyphIndex; i++)
                {
                    distance += GetGlyphAdvance(i, out _);
                }

                return distance;
            }
            else
            {
                //RightToLeft
                var glyphIndex = FindGlyphIndex(characterIndex);

                if (GlyphClusters != null)
                {
                    if (characterIndex > GlyphClusters[0])
                    {
                        return 0;
                    }

                    if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
                    {
                        return Size.Width;
                    }
                }

                for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
                {
                    distance += GetGlyphAdvance(i, out _);
                }

                return Size.Width - distance;
            }
        }

        /// <summary>
        /// Retrieves the <see cref="CharacterHit"/> value that represents the character hit of the caret of the <see cref="GlyphRun"/>.
        /// </summary>
        /// <param name="distance">Offset to use for computing the caret character hit.</param>
        /// <param name="isInside">Determines whether the character hit is inside the <see cref="GlyphRun"/>.</param>
        /// <returns>
        /// A <see cref="CharacterHit"/> value that represents the character hit that is closest to the distance value.
        /// The out parameter <c>isInside</c> returns <c>true</c> if the character hit is inside the <see cref="GlyphRun"/>; otherwise, <c>false</c>.
        /// </returns>
        public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
        {
            var characterIndex = 0;

            // Before
            if (distance <= 0)
            {
                isInside = false;

                if (GlyphClusters != null)
                {
                    characterIndex = GlyphClusters[characterIndex];
                }

                var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);

                return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
            }

            //After
            if (distance >= Size.Width)
            {
                isInside = false;

                characterIndex = GlyphIndices.Count - 1;

                if (GlyphClusters != null)
                {
                    characterIndex = GlyphClusters[characterIndex];
                }

                var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);

                return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
            }

            //Within
            var currentX = 0d;

            if (IsLeftToRight)
            {
                for (var index = 0; index < GlyphIndices.Count; index++)
                {
                    var advance = GetGlyphAdvance(index, out var cluster);

                    characterIndex = cluster;

                    if (distance > currentX && distance <= currentX + advance)
                    {
                        break;
                    }

                    currentX += advance;
                }
            }
            else
            {
                currentX = Size.Width;

                for (var index = GlyphIndices.Count - 1; index >= 0; index--)
                {
                    var advance = GetGlyphAdvance(index, out var cluster);

                    characterIndex = cluster;

                    if (currentX - advance < distance)
                    {
                        break;
                    }

                    currentX -= advance;
                }
            }

            isInside = true;

            var characterHit = FindNearestCharacterHit(characterIndex, out var width);

            var delta = width / 2;
            var offset = IsLeftToRight ? distance - currentX : currentX - distance;

            var isTrailing = offset > delta;

            return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
        }

        /// <summary>
        /// Retrieves the next valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
        /// </summary>
        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the next hit value.</param>
        /// <returns>
        /// A <see cref="CharacterHit"/> that represents the next valid caret character hit in the logical direction.
        /// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
        /// </returns>
        public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
        {
            if (characterHit.TrailingLength == 0)
            {
                characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);

                var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;

                return textPosition > _characters.End ?
                    characterHit :
                    new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
            }

            var nextCharacterHit =
                FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);

            if (characterHit == nextCharacterHit)
            {
                return characterHit;
            }

            return characterHit.TrailingLength > 0 ?
                nextCharacterHit :
                new CharacterHit(nextCharacterHit.FirstCharacterIndex);
        }

        /// <summary>
        /// Retrieves the previous valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
        /// </summary>
        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the previous hit value.</param>
        /// <returns>
        /// A cref="CharacterHit"/> that represents the previous valid caret character hit in the logical direction.
        /// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
        /// </returns>
        public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
        {
            if (characterHit.TrailingLength != 0)
            {
                return new CharacterHit(characterHit.FirstCharacterIndex);
            }

            var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);

            return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
        }

        /// <summary>
        /// Finds a glyph index for given character index.
        /// </summary>
        /// <param name="characterIndex">The character index.</param>
        /// <returns>
        /// The glyph index.
        /// </returns>
        public int FindGlyphIndex(int characterIndex)
        {
            if (GlyphClusters == null)
            {
                return characterIndex;
            }

            if (IsLeftToRight)
            {
                if (characterIndex < GlyphClusters[0])
                {
                    return 0;
                }

                if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
                {
                    return GlyphClusters.Count - 1;
                }
            }
            else
            {
                if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
                {
                    return GlyphClusters.Count - 1;
                }

                if (characterIndex > GlyphClusters[0])
                {
                    return 0;
                }
            }

            var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;

            var clusters = GlyphClusters;

            // Find the start of the cluster at the character index.
            var start = clusters.BinarySearch(characterIndex, comparer);

            // No cluster found.
            if (start < 0)
            {
                while (characterIndex > 0 && start < 0)
                {
                    characterIndex--;

                    start = clusters.BinarySearch(characterIndex, comparer);
                }

                if (start < 0)
                {
                    return -1;
                }
            }

            if (IsLeftToRight)
            {
                while (start > 0 && clusters[start - 1] == clusters[start])
                {
                    start--;
                }
            }
            else
            {
                while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
                {
                    start++;
                }
            }

            return start;
        }

        /// <summary>
        /// Finds the nearest <see cref="CharacterHit"/> at given index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="width">The width of found cluster.</param>
        /// <returns>
        /// The nearest <see cref="CharacterHit"/>.
        /// </returns>
        public CharacterHit FindNearestCharacterHit(int index, out double width)
        {
            width = 0.0;

            var start = FindGlyphIndex(index);

            if (GlyphClusters == null)
            {
                width = GetGlyphAdvance(index, out _);

                return new CharacterHit(start, 1);
            }

            var cluster = GlyphClusters[start];

            var nextCluster = cluster;

            var currentIndex = start;

            while (nextCluster == cluster)
            {
                width += GetGlyphAdvance(currentIndex, out _);

                if (IsLeftToRight)
                {
                    currentIndex++;

                    if (currentIndex == GlyphClusters.Count)
                    {
                        break;
                    }
                }
                else
                {
                    currentIndex--;

                    if (currentIndex < 0)
                    {
                        break;
                    }
                }

                nextCluster = GlyphClusters[currentIndex];
            }           

            int trailingLength;

            if (nextCluster == cluster)
            {
                trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
            }
            else
            {
                trailingLength = nextCluster - cluster;
            }

            return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
        }

        /// <summary>
        /// Gets a glyph's width.
        /// </summary>
        /// <param name="index">The glyph index.</param>
        /// <param name="cluster">The current cluster.</param>
        /// <returns>The glyph's width.</returns>
        private double GetGlyphAdvance(int index, out int cluster)
        {
            cluster = GlyphClusters != null ? GlyphClusters[index] : index;

            if (GlyphAdvances != null)
            {
                return GlyphAdvances[index];
            }

            var glyph = GlyphIndices[index];

            return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
        }

        /// <summary>
        /// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
        /// </summary>
        /// <returns>The baseline origin.</returns>
        private Point CalculateBaselineOrigin()
        {
            return new Point(0, -GlyphTypeface.Ascent * Scale);
        }

        private GlyphRunMetrics CreateGlyphRunMetrics()
        {
            if (GlyphClusters != null && GlyphClusters.Count > 0)
            {
                var firstCluster = GlyphClusters[0];

                _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
            }

            var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
            var widthIncludingTrailingWhitespace = 0d;

            var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);

            for (var index = 0; index < GlyphIndices.Count; index++)
            {
                var advance = GetGlyphAdvance(index, out _);

                widthIncludingTrailingWhitespace += advance;
            }

            var width = widthIncludingTrailingWhitespace;

            if (IsLeftToRight)
            {
                for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
                {
                    width -= GetGlyphAdvance(index, out _);
                }
            }
            else
            {
                for (var index = 0; index < glyphCount; index++)
                {
                    width -= GetGlyphAdvance(index, out _);
                }
            }

            return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
                height);
        }

        private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount)
        {
            glyphCount = 0;
            newLineLength = 0;

            if (Characters.IsEmpty)
            {
                return 0;
            }

            var trailingWhitespaceLength = 0;

            if (GlyphClusters == null)
            {
                for (var i = _characters.Length - 1; i >= 0;)
                {
                    var codepoint = Codepoint.ReadAt(_characters, i, out var count);

                    if (!codepoint.IsWhiteSpace)
                    {
                        break;
                    }

                    if (codepoint.IsBreakChar)
                    {
                        newLineLength++;
                    }

                    trailingWhitespaceLength++;

                    i -= count;
                    glyphCount++;
                }
            }
            else
            {
                for (var i = GlyphClusters.Count - 1; i >= 0; i--)
                {
                    var currentCluster = GlyphClusters[i];
                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);

                    if (!codepoint.IsWhiteSpace)
                    {
                        break;
                    }

                    var clusterLength = 1;

                    while(i - 1 >= 0)
                    {
                        var nextCluster = GlyphClusters[i - 1];

                        if(currentCluster == nextCluster)
                        {
                            clusterLength++;
                            i--;

                            continue;
                        }

                        break;
                    }

                    if (codepoint.IsBreakChar)
                    {
                        newLineLength += clusterLength;
                    }

                    trailingWhitespaceLength += clusterLength;
                   
                    glyphCount++;                   
                }
            }

            return trailingWhitespaceLength;
        }

        private void Set<T>(ref T field, T value)
        {
            if (_glyphRunImpl != null)
            {
                throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
            }

            _glyphRunMetrics = null;

            _baselineOrigin = null;

            field = value;
        }

        /// <summary>
        /// Initializes the <see cref="GlyphRun"/>.
        /// </summary>
        private void Initialize()
        {
            if (GlyphIndices == null)
            {
                throw new InvalidOperationException();
            }

            var glyphCount = GlyphIndices.Count;

            if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
            {
                throw new InvalidOperationException();
            }

            if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
            {
                throw new InvalidOperationException();
            }

            var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();

            _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this);
        }

        void IDisposable.Dispose()
        {
            _glyphRunImpl?.Dispose();
        }

        private class ReverseComparer<T> : IComparer<T>
        {
            public int Compare(T? x, T? y)
            {
                return Comparer<T>.Default.Compare(y, x);
            }
        }
    }
}
