/*
 * Copyright 2019 faddenSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace SourceGen {
    /// <summary>
    /// List of items formatted for display.
    /// </summary>
    /// <remarks>
    /// This is intended to be useful as an ItemSource for a WPF ListView.  We need to implement
    /// plain IList to cause ListView to perform data virtualization, and the property/collection
    /// changed events so the view will pick up our changes.
    /// 
    /// The ItemsControl.ItemsSource property wants an IEnumerable (which IList implements).
    /// According to various articles, if the object implements IList, and the UI element
    /// is providing *UI* virtualization, you will also get *data* virtualization.  This behavior
    /// doesn't seem to be documented anywhere, but the consensus is that it's expected to work.
    /// 
    /// Implementing generic IList&lt;&gt; doesn't seem necessary for XAML, but may be useful
    /// for other consumers of the data.
    /// 
    /// The list is initially filled with null references, with FormattedParts instances
    /// generated on demand.  This is done by requesting individual items from the
    /// LineListGen object.
    /// 
    /// NOTE: it may or may not be possible to implement this trivially with an
    /// ObservableCollection.  At an earlier iteration it wasn't, and I'd like to keep this
    /// around even if it is now possible, in case the pendulum swings back the other way.
    ///
    /// Additional reading on data virtualization:
    ///  https://www.codeproject.com/Articles/34405/WPF-Data-Virtualization?msg=5635751
    ///  https://web.archive.org/web/20121216034305/http://www.zagstudio.com/blog/498
    ///  https://web.archive.org/web/20121107200359/http://www.zagstudio.com/blog/378
    ///  https://github.com/lvaleriu/Virtualization
    /// </remarks>
    public class DisplayList : IList<DisplayList.FormattedParts>, IList,
            INotifyCollectionChanged, INotifyPropertyChanged {

        /// <summary>
        /// List of formatted parts.  DO NOT access this directly outside the event-sending
        /// method wrappers.
        /// </summary>
        private List<FormattedParts> mList;

        /// <summary>
        /// Data generation object.
        /// </summary>
        /// <remarks>
        /// This property is set by the LineListGen constructor.
        /// </remarks>
        public LineListGen ListGen { get; set; }

        /// <summary>
        /// Set of selected items, by list index.
        /// </summary>
        public DisplayListSelection SelectedIndices { get; private set; }


        /// <summary>
        /// Constructs an empty collection, with the default initial capacity.
        /// </summary>
        public DisplayList() {
            mList = new List<FormattedParts>();
            SelectedIndices = new DisplayListSelection();
        }


        #region Property / Collection Changed

        public event NotifyCollectionChangedEventHandler CollectionChanged;
        public event PropertyChangedEventHandler PropertyChanged;

        // See ObservableCollection class, e.g.
        // https://github.com/Microsoft/referencesource/blob/master/System/compmod/system/collections/objectmodel/observablecollection.cs

        private const string CountString = "Count";
        private const string IndexerName = "Item[]";

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
            PropertyChanged?.Invoke(this, e);
        }

        private void OnPropertyChanged(string propertyName) {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
            CollectionChanged?.Invoke(this, e);
        }

        private void OnCollectionChanged(NotifyCollectionChangedAction action,
                object item, int index) {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
        }

        private void OnCollectionChanged(NotifyCollectionChangedAction action,
                object item, int index, int oldIndex) {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));
        }

        private void OnCollectionChanged(NotifyCollectionChangedAction action,
                object oldItem, object newItem, int index) {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
        }

        private void OnCollectionReset() {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        #endregion Property / Collection Changed



        #region IList / IList<T>
        public int Count => ((IList<FormattedParts>)mList).Count;

        public bool IsReadOnly => ((IList<FormattedParts>)mList).IsReadOnly;

        public bool IsFixedSize => ((IList)mList).IsFixedSize;

        public object SyncRoot => ((IList)mList).SyncRoot;

        public bool IsSynchronized => ((IList)mList).IsSynchronized;

        public void Add(FormattedParts item) {
            ((IList<FormattedParts>)mList).Add(item);
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionReset();
        }

        public int Add(object value) {
            int posn = ((IList)mList).Add(value);
            if (posn >= 0) {
                OnPropertyChanged(CountString);
                OnPropertyChanged(IndexerName);
                OnCollectionChanged(NotifyCollectionChangedAction.Add, value, posn);
            }
            return posn;
        }

        public void Clear() {
            ((IList<FormattedParts>)mList).Clear();
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionReset();

            // Not strictly necessary, but does free up the memory sooner.
            SelectedIndices = new DisplayListSelection();
        }

        public bool Contains(FormattedParts item) {
            return ((IList<FormattedParts>)mList).Contains(item);
        }
        bool IList.Contains(object value) {
            return Contains((FormattedParts)value);
        }

        public void CopyTo(FormattedParts[] array, int arrayIndex) {
            ((IList<FormattedParts>)mList).CopyTo(array, arrayIndex);
        }

        public void CopyTo(Array array, int index) {
            ((IList)mList).CopyTo(array, index);
        }

        public IEnumerator<FormattedParts> GetEnumerator() {
            // Use the indexer, rather than mList's enumerator, to get on-demand string gen.
            for (int i = 0; i < Count; i++) {
                yield return this[i];
            }
        }

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

        public int IndexOf(FormattedParts item) {
            return ((IList<FormattedParts>)mList).IndexOf(item);
        }
        int IList.IndexOf(object value) {
            return IndexOf((FormattedParts)value);
        }

        public void Insert(int index, FormattedParts item) {
            ((IList<FormattedParts>)mList).Insert(index, item);
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
        }
        void IList.Insert(int index, object value) {
            Insert(index, (FormattedParts)value);
        }

        public void RemoveAt(int index) {
            FormattedParts removed = mList[index];
            ((IList<FormattedParts>)mList).RemoveAt(index);
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Remove, removed, index);
        }

        public bool Remove(FormattedParts item) {
            // NotifyCollectionChangedAction.Remove wants an index.  We can find the index
            // of the first matching item and then do a RemoveAt, but this call just isn't
            // all that interesting for us, so it's easier to ignore it.
            //return ((IList<FormattedParts>)mList).Remove(item);
            throw new NotSupportedException();
        }

        void IList.Remove(object value) {
            //Remove((FormattedParts)value);
            throw new NotSupportedException();
        }

        object IList.this[int index] {
            // forward to generic impl
            get { return this[index]; }
            set { this[index] = (FormattedParts)value; }
        }

        // For IList<T>.
        public FormattedParts this[int index] {
            get {
                FormattedParts parts = mList[index];
                if (parts == null) {
                    parts = mList[index] = GetEntry(index);
                }
                return parts;
            }
            set {
                FormattedParts orig = mList[index];
                mList[index] = value;
                OnPropertyChanged(IndexerName);
                OnCollectionChanged(NotifyCollectionChangedAction.Replace, orig, value, index);
            }
        }

        #endregion IList / IList<T>



        /// <summary>
        /// Retrieves the Nth element.
        /// </summary>
        private FormattedParts GetEntry(int index) {
            FormattedParts parts = mList[index];
            if (parts == null) {
                parts = mList[index] = ListGen.GetFormattedParts(index);
                parts.ListIndex = index;
            }
            return parts;
        }

        /// <summary>
        /// Resets the list, filling it with empty elements.  Also resets the selected indices.
        /// </summary>
        /// <param name="size">New size of the list.</param>
        public void ResetList(int size) {
            // TODO: can we recycle existing elements and just add/trim as needed?
            Clear();
            mList.Capacity = size;
            for (int i = 0; i < size; i++) {
                // add directly to list so we don't send events
                mList.Add(null);
            }

            SelectedIndices = new DisplayListSelection(size);

            // send one big notification at the end; "reset" means "forget everything you knew"
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionReset();
        }

        /// <summary>
        /// A range of lines has been replaced with a new range of lines.  The new set may be
        /// the same size, larger, or smaller than the previous.
        /// </summary>
        /// <param name="startIndex">Start index of change area.</param>
        /// <param name="oldCount">Number of old lines.</param>
        /// <param name="newCount">Number of new lines.  May be zero.</param>
        public void ClearListSegment(int startIndex, int oldCount, int newCount) {
            Debug.WriteLine("ClearListSegment start=" + startIndex + " old=" + oldCount +
                " new=" + newCount + " (mList.Count=" + mList.Count + ")");

            Debug.Assert(startIndex >= 0 && startIndex < mList.Count);
            Debug.Assert(oldCount >= 0 && startIndex + oldCount <= mList.Count);
            Debug.Assert(newCount >= 0);

            // Remove the old elements to clear them.
            if (oldCount != 0) {
                mList.RemoveRange(startIndex, oldCount);
            }
            // Replace with the appropriate number of null entries.
            for (int i = 0; i < newCount; i++) {
                mList.Insert(startIndex, null);
            }
            // TODO(someday): can we null out existing entries, and just insert/remove when
            //   counts differ?

            if (oldCount != newCount) {
                SelectedIndices = new DisplayListSelection(mList.Count);
                RecalculateListIndices();
            }

            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            // TODO: this causes the ListView to format the entire listing, despite
            //   being virtual.  So we're regenerating the entire list after something trivial,
            //   like renaming a label, which hampers performance.  Need to figure this out.
            OnCollectionReset();
        }

        /// <summary>
        /// Recalculates the list index fields after lines are added or removed.
        /// </summary>
        private void RecalculateListIndices() {
            Debug.WriteLine("Recalculating list indices");
            for (int i = 0; i < mList.Count; i++) {
                if (mList[i] != null) {
                    mList[i].ListIndex = i;
                }
            }
        }

        /// <summary>
        /// List elements.  Instances are immutable except for ListIndex.
        /// </summary>
        public class FormattedParts {
            public string Offset { get; private set; }
            public string Addr { get; private set; }
            public string Bytes { get; private set; }
            public string Flags { get; private set; }
            public string Attr { get; private set; }
            public string Label { get; private set; }
            public string Opcode { get; private set; }
            public string Operand { get; private set; }
            public string Comment { get; private set; }
            public bool IsLongComment { get; private set; }

            public bool HasBackgroundColor { get; private set; }
            public Brush BackgroundBrush { get; private set; }

            public bool IsVisualizationSet { get; private set; }
            public Visualization[] VisualizationSet { get; private set; }

            // Set to true if we want to highlight the address and label fields.  This is
            // examined by a data trigger in CodeListItemStyle.xaml.
            public bool HasAddrLabelHighlight { get; private set; }

            // Set to true if we want to highlight the operand field.  This is
            // examined by a data trigger in CodeListItemStyle.xaml.
            public bool HasOperandHighlight { get; private set; }

            // Set to true if the Flags field has been modified.
            public bool HasModifiedFlags {
                get { return (mPartFlags & PartFlags.HasModifiedFlags) != 0; }
            }
            // Set to true if the address here is actually non-addressable.
            public bool IsNonAddressable {
                get { return (mPartFlags & PartFlags.IsNonAddressable) != 0; }
            }

            // List index, filled in on demand.  If the list is regenerated we want to
            // renumber elements without having to recreate them, so this field is mutable.
            public int ListIndex { get; set; } = -1;

            [Flags]
            public enum PartFlags {
                None = 0,
                HasModifiedFlags = 1,        // Flags field is non-default
                IsNonAddressable = 1 << 1,   // this is a non-addressable region
            }
            private PartFlags mPartFlags;


            private static Color NoColor = CommonWPF.Helper.ZeroColor;


            // Private constructor -- create instances with factory methods.
            private FormattedParts() {
                Offset = Addr = Bytes = Flags = Attr = Label = Opcode = Operand = Comment =
                    string.Empty;
            }

            /// <summary>
            /// Clones the specified object.
            /// </summary>
            private static FormattedParts Clone(FormattedParts orig) {
                FormattedParts newParts = FormattedParts.Create(orig.Offset, orig.Addr,
                    orig.Bytes, orig.Flags, orig.Attr, orig.Label, orig.Opcode, orig.Operand,
                    orig.Comment, orig.mPartFlags);

                newParts.IsLongComment = orig.IsLongComment;
                newParts.HasAddrLabelHighlight = orig.HasAddrLabelHighlight;

                newParts.ListIndex = orig.ListIndex;
                return newParts;
            }

            public static FormattedParts Create(string offset, string addr, string bytes,
                    string flags, string attr, string label, string opcode, string operand,
                    string comment, PartFlags pflags) {
                FormattedParts parts = new FormattedParts();
                parts.Offset = offset;
                parts.Addr = addr;
                parts.Bytes = bytes;
                parts.Flags = flags;
                parts.Attr = attr;
                parts.Label = label;
                parts.Opcode = opcode;
                parts.Operand = operand;
                parts.Comment = comment;
                parts.IsLongComment = false;
                parts.mPartFlags = pflags;

                return parts;
            }

            public static FormattedParts CreateBlankLine() {
                FormattedParts parts = new FormattedParts();
                return parts;
            }

            public static FormattedParts CreateLongComment(string comment) {
                FormattedParts parts = new FormattedParts();
                parts.Comment = comment;
                parts.IsLongComment = true;
                return parts;
            }

            public static FormattedParts CreateNote(string comment, Color color) {
                FormattedParts parts = new FormattedParts();
                parts.Comment = comment;
                parts.IsLongComment = true;
                if (color != NoColor) {
                    parts.HasBackgroundColor = true;
                    parts.BackgroundBrush = new SolidColorBrush(color);
                    parts.BackgroundBrush.Freeze();     // export runs on non-UI thread
                }
                return parts;
            }

            public static FormattedParts CreateDirective(string opstr, string operandStr) {
                FormattedParts parts = new FormattedParts();
                parts.Opcode = opstr;
                parts.Operand = operandStr;
                return parts;
            }

            public static FormattedParts CreatePreLabelDirective(string addrStr, string labelStr) {
                FormattedParts parts = new FormattedParts();
                parts.Addr = addrStr;
                parts.Label = labelStr;
                return parts;
            }

            public static FormattedParts CreateFullDirective(string label, string opstr,
                    string operandStr, string comment) {
                FormattedParts parts = new FormattedParts();
                parts.Label = label;
                parts.Opcode = opstr;
                parts.Operand = operandStr;
                parts.Comment = comment;
                return parts;
            }

            public static FormattedParts CreateVisualizationSet(VisualizationSet visSet) {
                FormattedParts parts = new FormattedParts();
                if (visSet.Count == 0) {
                    // should not happen
                    parts.Comment = "!EMPTY VSET!";
                    parts.IsLongComment = true;
                } else {
                    string fmt;
                    if (visSet.Count == 1) {
                        fmt = Res.Strings.VIS_SET_SINGLE_FMT;
                    } else {
                        fmt = Res.Strings.VIS_SET_MULTIPLE_FMT;
                    }
                    parts.Comment = string.Format(fmt, "Bitmap", visSet[0].Tag, visSet.Count - 1);
                    parts.VisualizationSet = visSet.ToArray();
                    parts.IsVisualizationSet = true;
                }
                return parts;
            }

            public static FormattedParts AddSelectionAddrHighlight(FormattedParts orig) {
                FormattedParts newParts = Clone(orig);
                newParts.HasAddrLabelHighlight = true;
                return newParts;
            }

            public static FormattedParts RemoveSelectionAddrHighlight(FormattedParts orig) {
                FormattedParts newParts = Clone(orig);
                newParts.HasAddrLabelHighlight = false;
                return newParts;
            }

            public static FormattedParts AddSelectionOperHighlight(FormattedParts orig) {
                FormattedParts newParts = Clone(orig);
                newParts.HasOperandHighlight = true;
                return newParts;
            }

            public static FormattedParts RemoveSelectionOperHighlight(FormattedParts orig) {
                FormattedParts newParts = Clone(orig);
                newParts.HasOperandHighlight = false;
                return newParts;
            }

            public override string ToString() {
                return "[Parts: index=" + ListIndex + " off=" + Offset + "]";
            }
        }
    }
}
