using System.Collections.Generic;
using System.Linq;

namespace ZeroLevel.DataStructures
{
    /// <summary>
    /// Represents a Sparse matrix.
    /// </summary>
    /// <typeparam name="T">The type of the stored value.</typeparam>
    public class SparseMatrix<T>
    {
        /// <summary>
        /// Dictionary containing the row index as a key and as a value another dictionary
        /// containing the column index as a key and the stored value as a value.
        /// </summary>
        internal Dictionary<int, Dictionary<int, T>> rows;

        /// <summary>
        /// Dictionary containing the column index as a key and as a value another dictionary
        /// containing the row index as a key and the stored value as a value.
        /// </summary>
        internal Dictionary<int, Dictionary<int, T>> cols;

        /// <summary>
        /// Gets the maximum reached height of the sparse matrix.
        /// </summary>
        public int Height { get; internal set; }

        /// <summary>
        /// Gets the maximum reached width of the sparse matrix.
        /// </summary>
        public int Width { get; internal set; }

        /// <summary>
        /// Gets the number of items in the <see cref="SparseMatrix{T}"/>.
        /// </summary>
        public int Count { get; internal set; }

        /// <summary>
        /// Gets or sets an item in the sparse matrix. If there is no item on the given position
        /// on get the default value of T is returned and on set the item is added to the matrix.
        /// </summary>
        /// <param name="row">The zero-based row index of the item.</param>
        /// <param name="col">The zero-based column index of the item.</param>
        /// <returns>Returns the item in the sparse matrix. If there is no item on the given position the default value of T is returned instead.</returns>
        public T this[int row, int col]
        {
            get
            {
                if (rows.ContainsKey(row))
                {
                    if (rows[row].ContainsKey(col))
                    {
                        return rows[row][col];
                    }
                }

                //If there is no item on the given position return defaault value
                return default(T)!;
            }
            set
            {
                if (row >= Height) Height = row + 1;
                if (col >= Width) Width = col + 1;

                //If no items on the current row we have to create a new dictionary
                if (!rows.ContainsKey(row))
                    rows.Add(row, new Dictionary<int, T>());

                //If no items on the current col we have to create a new dictionary
                if (!cols.ContainsKey(col))
                    cols.Add(col, new Dictionary<int, T>());

                rows[row][col] = value;
                cols[col][row] = value;

                Count++;
            }
        }

        /// <summary>
        /// Creates a new instance of the <see cref="SparseMatrix{T}"/> class.
        /// </summary>
        public SparseMatrix()
        {
            rows = new Dictionary<int, Dictionary<int, T>>();
            cols = new Dictionary<int, Dictionary<int, T>>();
        }

        /// <summary>
        /// Creates a new instance of the <see cref="SparseMatrix{T}"/> class from the given two dimensional array.
        /// </summary>
        /// <param name="array">The two dimensional array of items to add.</param>
        /// <param name="zeroItem">The item considered a zero item. All items from the array equal to the zero item won't be added to the matrix.</param>
        public SparseMatrix(T[,] array, T zeroItem)
        {
            rows = new Dictionary<int, Dictionary<int, T>>();
            cols = new Dictionary<int, Dictionary<int, T>>();

            for (int row = 0; row < array.GetLength(0); row++)
            {
                for (int col = 0; col < array.GetLength(1); col++)
                {
                    if (!object.Equals(array[row, col], zeroItem))
                        this[row, col] = array[row, col];
                }
            }
        }

        /// <summary>
        /// Determines if there is an item on the given position.
        /// </summary>
        /// <param name="row">The zero-based row index of the item.</param>
        /// <param name="col">The zero-based column index of the item.</param>
        /// <returns>Returns true if there is an item on the given position; otherwise false.</returns>
        public bool IsCellEmpty(int row, int col)
        {
            if (rows.ContainsKey(row))
            {
                if (rows[row].ContainsKey(col))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Gets the items in the given row sorted by the column index as an <see cref="IEnumerable{T}"/>
        /// of <see cref="KeyValuePair{TKey, TValue}"/> with the key being the column index and the value being the item.
        /// </summary>
        /// <param name="row">The zero-based row index.</param>
        /// <returns>Returns an <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{TKey, TValue}"/>
        /// with the key being the column index and the value being the item.</returns>
        public IEnumerable<KeyValuePair<int, T>> GetRowItems(int row)
        {
            if (rows.ContainsKey(row))
            {
                var sortedDict = new SortedDictionary<int, T>(rows[row]);
                foreach (var item in sortedDict)
                {
                    yield return item;
                }
            }
        }

        /// <summary>
        /// Gets the items in the given column sorted by the row index as an <see cref="IEnumerable{T}"/>
        /// of <see cref="KeyValuePair{TKey, TValue}"/> with the key being the row index and the value being the item.
        /// </summary>
        /// <param name="col">The zero-based column index.</param>
        /// <returns>Returns an <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{TKey, TValue}"/>
        /// with the key being the row index and the value being the item.</returns>
        public IEnumerable<KeyValuePair<int, T>> GetColumnItems(int col)
        {
            if (cols.ContainsKey(col))
            {
                var sortedDict = new SortedDictionary<int, T>(cols[col]);
                foreach (var item in sortedDict)
                {
                    yield return item;
                }
            }
        }

        /// <summary>
        /// Gets non empty rows indexes sorted in ascending order.
        /// </summary>
        /// <returns>Returns an <see cref="IEnumerable{T}"/> of integers being row indexes sorted in ascending order.</returns>
        public IEnumerable<int> GetNonEmptyRows()
        {
            var sortedRows = new SortedSet<int>(rows.Keys);
            foreach (var row in sortedRows)
            {
                yield return row;
            }
        }

        /// <summary>
        /// Gets non empty columns indexes sorted in ascending order.
        /// </summary>
        /// <returns>Returns an <see cref="IEnumerable{T}"/> of integers being column indexes sorted in ascending order.</returns>
        public IEnumerable<int> GetNonEmptyColumns()
        {
            var sortedCols = new SortedSet<int>(cols.Keys);
            foreach (var col in sortedCols)
            {
                yield return col;
            }
        }

        /// <summary>
        /// Removes the item on the given position.
        /// </summary>
        /// <param name="row">The zero-based row index.</param>
        /// <param name="col">The zero-based column index.</param>
        /// <returns>Returns true if item is removed successfully; otherwise false. Also returns false if the item is not found.</returns>
        public bool Remove(int row, int col)
        {
            if (rows.ContainsKey(row))
            {
                if (rows[row].ContainsKey(col))
                {
                    bool removedSuccessfully = true;
                    if (!rows[row].Remove(col) || !cols[col].Remove(row)) removedSuccessfully = false;

                    if (rows[row].Count == 0)
                    {
                        rows.Remove(row);
                    }

                    if (cols[col].Count == 0)
                    {
                        cols.Remove(col);
                    }

                    if (removedSuccessfully)
                        Count--;

                    return removedSuccessfully;
                }
            }

            return false;
        }

        /// <summary>
        /// Removes all elements from the sparse matrix.
        /// </summary>
        public void Clear()
        {
            rows.Clear();
            cols.Clear();

            Count = 0;
            Height = 0;
            Width = 0;
        }

        /// <summary>
        /// Updates the height and the width of the matrix. If no items were removed from the matrix the dimensions will be correct.
        /// </summary>
        public void UpdateDimensions()
        {
            if (rows.Count == 0)
            {
                Height = 0;
                Width = 0;
                return;
            }

            Height = rows.Keys.Max() + 1;
            Width = cols.Keys.Max() + 1;
        }
    }
}
