// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.FlowAnalysis.SymbolUsageAnalysis
{
    internal static partial class SymbolUsageAnalysis
    {
        /// <summary>
        /// Analysis data for a particular <see cref="BasicBlock"/> for <see cref="ControlFlowGraph"/>
        /// based dataflow analysis OR for the entire executable code block for high level operation
        /// tree based analysis.
        /// </summary>
        private sealed class BasicBlockAnalysisData : IDisposable
        {
            private static readonly ObjectPool<BasicBlockAnalysisData> s_pool =
                new(() => new BasicBlockAnalysisData());

            /// <summary>
            /// Map from each symbol to possible set of reachable write operations that are live at current program point.
            /// A write is live if there is no intermediate write operation that overwrites it.
            /// </summary>
            private readonly Dictionary<ISymbol, PooledHashSet<IOperation>> _reachingWrites;

            private BasicBlockAnalysisData()
                => _reachingWrites = new Dictionary<ISymbol, PooledHashSet<IOperation>>();

            public static BasicBlockAnalysisData GetInstance() => s_pool.Allocate();

            public void Dispose()
            {
                FreeAndClearValues();
                s_pool.Free(this);
            }

            private void FreeAndClearValues()
            {
                foreach (var value in _reachingWrites.Values)
                {
                    value.Free();
                }

                _reachingWrites.Clear();
            }

            public void SetAnalysisDataFrom(BasicBlockAnalysisData other)
            {
                if (ReferenceEquals(this, other))
                {
                    return;
                }

                FreeAndClearValues();
                AddEntries(_reachingWrites, other);
            }

            public void Clear(ISymbol symbol)
            {
                if (_reachingWrites.TryGetValue(symbol, out var value))
                {
                    value.Free();
                    _reachingWrites.Remove(symbol);
                }
            }

            /// <summary>
            /// Gets the currently reachable writes for the given symbol.
            /// </summary>
            public IEnumerable<IOperation> GetCurrentWrites(ISymbol symbol)
            {
                if (_reachingWrites.TryGetValue(symbol, out var values))
                {
                    foreach (var value in values)
                    {
                        yield return value;
                    }
                }
            }

            /// <summary>
            /// Marks the given symbol write as a new unread write operation,
            /// potentially clearing out the prior write operations if <paramref name="maybeWritten"/> is <code>false</code>.
            /// </summary>
            public void OnWriteReferenceFound(ISymbol symbol, IOperation operation, bool maybeWritten)
            {
                if (!_reachingWrites.TryGetValue(symbol, out var values))
                {
                    values = PooledHashSet<IOperation>.GetInstance();
                    _reachingWrites.Add(symbol, values);
                }
                else if (!maybeWritten)
                {
                    values.Clear();
                }

                values.Add(operation);
            }

            public bool Equals(BasicBlockAnalysisData other)
            {
                // Check if both _reachingWrites maps have same key-value pair count.
                if (other == null ||
                    other._reachingWrites.Count != _reachingWrites.Count)
                {
                    return false;
                }

                var uniqueSymbols = PooledHashSet<ISymbol>.GetInstance();
                try
                {
                    // Check if both _reachingWrites maps have same set of keys.
                    uniqueSymbols.AddRange(_reachingWrites.Keys);
                    uniqueSymbols.AddRange(other._reachingWrites.Keys);
                    if (uniqueSymbols.Count != _reachingWrites.Count)
                    {
                        return false;
                    }

                    // Check if both _reachingWrites maps have same set of write
                    // operations for each tracked symbol.
                    foreach (var symbol in uniqueSymbols)
                    {
                        var writes1 = _reachingWrites[symbol];
                        var writes2 = other._reachingWrites[symbol];
                        if (!writes1.SetEquals(writes2))
                        {
                            return false;
                        }
                    }

                    return true;
                }
                finally
                {
                    uniqueSymbols.Free();
                }
            }

            private bool IsEmpty => _reachingWrites.Count == 0;

            public static BasicBlockAnalysisData Merge(
                BasicBlockAnalysisData data1,
                BasicBlockAnalysisData data2,
                Action<BasicBlockAnalysisData> trackAllocatedData)
            {
                // Ensure that we don't return 'null' data if other the other data is non-null,
                // even if latter is Empty.
                if (data1 == null)
                {
                    return data2;
                }
                else if (data2 == null)
                {
                    return data1;
                }
                else if (data1.IsEmpty)
                {
                    return data2;
                }
                else if (data2.IsEmpty)
                {
                    return data1;
                }
                else if (data1.Equals(data2))
                {
                    return data1;
                }

                var mergedData = GetInstance();
                AddEntries(mergedData._reachingWrites, data1);
                AddEntries(mergedData._reachingWrites, data2);

                if (mergedData.Equals(data1))
                {
                    mergedData.Dispose();
                    return data1;
                }
                else if (mergedData.Equals(data2))
                {
                    mergedData.Dispose();
                    return data2;
                }

                trackAllocatedData(mergedData);
                return mergedData;
            }

            private static void AddEntries(Dictionary<ISymbol, PooledHashSet<IOperation>> result, BasicBlockAnalysisData source)
            {
                if (source != null)
                {
                    foreach (var (symbol, operations) in source._reachingWrites)
                    {
                        if (!result.TryGetValue(symbol, out var values))
                        {
                            values = PooledHashSet<IOperation>.GetInstance();
                            result.Add(symbol, values);
                        }

                        values.AddRange(operations);
                    }
                }
            }
        }
    }
}
