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

namespace ClosedXML.Excel
{
    using System.Linq;

    internal class XLCells : IXLCells, IXLStylized, IEnumerable<XLCell>
    {
        public Boolean StyleChanged { get; set; }
        #region Fields

        private readonly bool _includeFormats;
        private readonly List<XLRangeAddress> _rangeAddresses = new List<XLRangeAddress>();
        private readonly bool _usedCellsOnly;
        private IXLStyle _style;
        private readonly Func<IXLCell, Boolean> _predicate;
        #endregion

        #region Constructor

        public XLCells(bool usedCellsOnly, bool includeFormats, Func<IXLCell, Boolean> predicate = null)
        {
            _style = new XLStyle(this, XLWorkbook.DefaultStyle);
            _usedCellsOnly = usedCellsOnly;
            _includeFormats = includeFormats;
            _predicate = predicate;
        }

        #endregion

        #region IEnumerable<XLCell> Members

        public IEnumerator<XLCell> GetEnumerator()
        {
            var cellsInRanges = new Dictionary<XLWorksheet, HashSet<XLSheetPoint>>();
            Boolean oneRange = _rangeAddresses.Count == 1;
            foreach (XLRangeAddress range in _rangeAddresses)
            {
                HashSet<XLSheetPoint> hash;
                if (cellsInRanges.ContainsKey(range.Worksheet))
                    hash = cellsInRanges[range.Worksheet];
                else
                {
                    hash = new HashSet<XLSheetPoint>();
                    cellsInRanges.Add(range.Worksheet, hash);
                }

                if (_usedCellsOnly)
                {
                    if (oneRange)
                    {
                        var cellRange = range
                            .Worksheet
                            .Internals
                            .CellsCollection
                            .GetCells(
                                range.FirstAddress.RowNumber,
                                range.FirstAddress.ColumnNumber,
                                range.LastAddress.RowNumber,
                                range.LastAddress.ColumnNumber)
                            .Where(c => !c.IsEmpty(_includeFormats)
                                        && (_predicate == null || _predicate(c))
                            );

                        foreach(var cell in cellRange)
                        {
                            yield return cell;
                        }
                    }
                    else
                    {
                        var tmpRange = range;
                        var addressList = range.Worksheet.Internals.CellsCollection
                            .GetSheetPoints(
                            tmpRange.FirstAddress.RowNumber,
                            tmpRange.FirstAddress.ColumnNumber,
                            tmpRange.LastAddress.RowNumber,
                            tmpRange.LastAddress.ColumnNumber);

                        foreach (XLSheetPoint a in addressList.Where(a => !hash.Contains(a)))
                        {
                            hash.Add(a);
                        }
                    }
                }
                else
                {
                    var mm = new MinMax
                                 {
                                     MinRow = range.FirstAddress.RowNumber,
                                     MaxRow = range.LastAddress.RowNumber,
                                     MinColumn = range.FirstAddress.ColumnNumber,
                                     MaxColumn = range.LastAddress.ColumnNumber
                                 };
                    if (mm.MaxRow > 0 && mm.MaxColumn > 0)
                    {
                        for (Int32 ro = mm.MinRow; ro <= mm.MaxRow; ro++)
                        {
                            for (Int32 co = mm.MinColumn; co <= mm.MaxColumn; co++)
                            {
                                if (oneRange)
                                {
                                    var c = range.Worksheet.Cell(ro, co);
                                    if (_predicate == null || _predicate(c))
                                        yield return c;
                                }
                                else
                                {
                                    var address = new XLSheetPoint(ro, co);
                                    if (!hash.Contains(address))
                                        hash.Add(address);
                                }
                            }
                        }
                    }
                }
            }

            if (!oneRange)
            {
                if (_usedCellsOnly)
                {
                    var cellRange = cellsInRanges
                        .SelectMany(
                            cir =>
                            cir.Value.Select(a => cir.Key.Internals.CellsCollection.GetCell(a)).Where(
                                cell =>
                                    cell != null
                                    && !cell.IsEmpty(_includeFormats)
                                    && (_predicate == null || _predicate(cell))
                            )
                    );

                    foreach (var cell in cellRange)
                    {
                        yield return cell;
                    }
                }
                else
                {
                    foreach (var cir in cellsInRanges)
                    {
                        foreach (XLSheetPoint a in cir.Value)
                        {
                            var c = cir.Key.Cell(a.Row, a.Column);
                            if (_predicate == null || _predicate(c))
                                yield return c;
                        }
                    }
                }
            }
        }

        #endregion

        #region IXLCells Members

        IEnumerator<IXLCell> IEnumerable<IXLCell>.GetEnumerator()
        {
            foreach (XLCell cell in this)
                yield return cell;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public IXLStyle Style
        {
            get { return _style; }
            set
            {
                _style = new XLStyle(this, value);
                this.ForEach<XLCell>(c => c.Style = _style);
            }
        }

        public Object Value
        {
            set { this.ForEach<XLCell>(c => c.Value = value); }
        }

        public IXLCells SetDataType(XLDataType dataType)
        {
            this.ForEach<XLCell>(c => c.DataType = dataType);
            return this;
        }

        public XLDataType DataType
        {
            set { this.ForEach<XLCell>(c => c.DataType = value); }
        }


        public IXLCells Clear(XLClearOptions clearOptions = XLClearOptions.ContentsAndFormats)
        {
            this.ForEach<XLCell>(c => c.Clear(clearOptions));
            return this;
        }

        public void DeleteComments() {
            this.ForEach<XLCell>(c => c.DeleteComment());
        }

        public String FormulaA1
        {
            set { this.ForEach<XLCell>(c => c.FormulaA1 = value); }
        }

        public String FormulaR1C1
        {
            set { this.ForEach<XLCell>(c => c.FormulaR1C1 = value); }
        }

        #endregion

        #region IXLStylized Members

        public IEnumerable<IXLStyle> Styles
        {
            get
            {
                UpdatingStyle = true;
                yield return _style;
                foreach (XLCell c in this)
                    yield return c.Style;
                UpdatingStyle = false;
            }
        }

        public Boolean UpdatingStyle { get; set; }

        public IXLStyle InnerStyle
        {
            get { return _style; }
            set { _style = new XLStyle(this, value); }
        }

        public IXLRanges RangesUsed
        {
            get
            {
                var retVal = new XLRanges();
                this.ForEach<XLCell>(c => retVal.Add(c.AsRange()));
                return retVal;
            }
        }

        #endregion

        public void Add(XLRangeAddress rangeAddress)
        {
            _rangeAddresses.Add(rangeAddress);
        }

        public void Add(XLCell cell)
        {
            _rangeAddresses.Add(new XLRangeAddress(cell.Address, cell.Address));
        }

        //--

        #region Nested type: MinMax

        private struct MinMax
        {
            public Int32 MaxColumn;
            public Int32 MaxRow;
            public Int32 MinColumn;
            public Int32 MinRow;
        }

        #endregion

        public void Select()
        {
            foreach (var cell in this)
                cell.Select();
        }
    }
}
