// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation;

using Microsoft.PowerShell.Commands.Internal.Format;

namespace Microsoft.PowerShell.Commands
{
    /// <summary>
    /// </summary>
    [Cmdlet(VerbsData.Compare, "Object", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096605",
        RemotingCapability = RemotingCapability.None)]
    public sealed class CompareObjectCommand : ObjectCmdletBase
    {
        #region Parameters
        /// <summary>
        /// </summary>
        [Parameter(Position = 0, Mandatory = true)]
        [AllowEmptyCollection]
        public PSObject[] ReferenceObject { get; set; }

        /// <summary>
        /// </summary>
        [Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true)]
        [AllowEmptyCollection]
        public PSObject[] DifferenceObject { get; set; }

        /// <summary>
        /// </summary>
        [Parameter]
        [ValidateRange(0, int.MaxValue)]
        public int SyncWindow { get; set; } = int.MaxValue;

        /// <summary>
        /// </summary>
        /// <value></value>
        [Parameter]
        public object[] Property { get; set; }

        /* not implemented
        /// <summary>
        /// </summary>
        [Parameter]
        public SwitchParameter IgnoreWhiteSpace
        {
            get { return _ignoreWhiteSpace; }

            set { _ignoreWhiteSpace = value; }
        }

        private bool _ignoreWhiteSpace = false;
        */

        /// <summary>
        /// </summary>
        [Parameter]
        public SwitchParameter ExcludeDifferent
        {
            get { return _excludeDifferent; }

            set { _excludeDifferent = value; }
        }

        private bool _excludeDifferent /*=false*/;

        /// <summary>
        /// </summary>
        [Parameter]
        public SwitchParameter IncludeEqual
        {
            get
            {
                return _includeEqual;
            }

            set
            {
                _isIncludeEqualSpecified = true;
                _includeEqual = value;
            }
        }

        private bool _includeEqual /* = false */;
        private bool _isIncludeEqualSpecified /* = false */;

        /// <summary>
        /// </summary>
        [Parameter]
        public SwitchParameter PassThru
        {
            get { return _passThru; }

            set { _passThru = value; }
        }

        private bool _passThru /* = false */;
        #endregion Parameters

        #region Internal
        private List<OrderByPropertyEntry> _referenceEntries;

        private readonly List<OrderByPropertyEntry> _referenceEntryBacklog
            = new();

        private readonly List<OrderByPropertyEntry> _differenceEntryBacklog
            = new();

        private OrderByProperty _orderByProperty = null;
        private OrderByPropertyComparer _comparer = null;

        private int _referenceObjectIndex /* = 0 */;

        // These are programmatic strings, not subject to INTL
        private const string SideIndicatorPropertyName = "SideIndicator";
        private const string SideIndicatorMatch = "==";
        private const string SideIndicatorReference = "<=";
        private const string SideIndicatorDifference = "=>";
        private const string InputObjectPropertyName = "InputObject";

        /// <summary>
        /// The following is the matching algorithm:
        /// Retrieve the incoming object (differenceEntry) if any
        /// Retrieve the next reference object (referenceEntry) if any
        /// If differenceEntry matches referenceEntry
        ///   Emit referenceEntry as a match
        ///   Return
        /// If differenceEntry matches any entry in referenceEntryBacklog
        ///   Emit the backlog entry as a match
        ///   Remove the backlog entry from referenceEntryBacklog
        ///   Clear differenceEntry
        /// If referenceEntry (if any) matches any entry in differenceEntryBacklog
        ///   Emit referenceEntry as a match
        ///   Remove the backlog entry from differenceEntryBacklog
        ///   Clear referenceEntry
        /// If differenceEntry is still present
        ///   If SyncWindow is 0
        ///     Emit differenceEntry as unmatched
        ///   Else
        ///     While there is no space in differenceEntryBacklog
        ///       Emit oldest entry in differenceEntryBacklog as unmatched
        ///       Remove oldest entry from differenceEntryBacklog
        ///     Add differenceEntry to differenceEntryBacklog
        /// If referenceEntry is still present
        ///   If SyncWindow is 0
        ///     Emit referenceEntry as unmatched
        ///   Else
        ///     While there is no space in referenceEntryBacklog
        ///       Emit oldest entry in referenceEntryBacklog as unmatched
        ///       Remove oldest entry from referenceEntryBacklog
        ///     Add referenceEntry to referenceEntryBacklog.
        /// </summary>
        /// <param name="differenceEntry"></param>
        private void Process(OrderByPropertyEntry differenceEntry)
        {
            Diagnostics.Assert(_referenceEntries != null, "null referenceEntries");

            // Retrieve the next reference object (referenceEntry) if any
            OrderByPropertyEntry referenceEntry = null;
            if (_referenceObjectIndex < _referenceEntries.Count)
            {
                referenceEntry = _referenceEntries[_referenceObjectIndex++];
            }

            // If differenceEntry matches referenceEntry
            //   Emit referenceEntry as a match
            //   Return
            // 2005/07/19 Switched order of referenceEntry and differenceEntry
            //   so that we cast differenceEntry to the type of referenceEntry.
            if (referenceEntry != null && differenceEntry != null &&
                _comparer.Compare(referenceEntry, differenceEntry) == 0)
            {
                EmitMatch(referenceEntry);
                return;
            }

            // If differenceEntry matches any entry in referenceEntryBacklog
            //   Emit the backlog entry as a match
            //   Remove the backlog entry from referenceEntryBacklog
            //   Clear differenceEntry
            OrderByPropertyEntry matchingEntry =
                MatchAndRemove(differenceEntry, _referenceEntryBacklog);
            if (matchingEntry != null)
            {
                EmitMatch(matchingEntry);
                differenceEntry = null;
            }

            // If referenceEntry (if any) matches any entry in differenceEntryBacklog
            //   Emit referenceEntry as a match
            //   Remove the backlog entry from differenceEntryBacklog
            //   Clear referenceEntry
            matchingEntry =
                MatchAndRemove(referenceEntry, _differenceEntryBacklog);
            if (matchingEntry != null)
            {
                EmitMatch(referenceEntry);
                referenceEntry = null;
            }

            // If differenceEntry is still present
            //   If SyncWindow is 0
            //     Emit differenceEntry as unmatched
            //   Else
            //     While there is no space in differenceEntryBacklog
            //       Emit oldest entry in differenceEntryBacklog as unmatched
            //       Remove oldest entry from differenceEntryBacklog
            //     Add differenceEntry to differenceEntryBacklog
            if (differenceEntry != null)
            {
                if (SyncWindow > 0)
                {
                    while (_differenceEntryBacklog.Count >= SyncWindow)
                    {
                        EmitDifferenceOnly(_differenceEntryBacklog[0]);
                        _differenceEntryBacklog.RemoveAt(0);
                    }

                    _differenceEntryBacklog.Add(differenceEntry);
                }
                else
                {
                    EmitDifferenceOnly(differenceEntry);
                }
            }

            // If referenceEntry is still present
            //   If SyncWindow is 0
            //     Emit referenceEntry as unmatched
            //   Else
            //     While there is no space in referenceEntryBacklog
            //       Emit oldest entry in referenceEntryBacklog as unmatched
            //       Remove oldest entry from referenceEntryBacklog
            //     Add referenceEntry to referenceEntryBacklog
            if (referenceEntry != null)
            {
                if (SyncWindow > 0)
                {
                    while (_referenceEntryBacklog.Count >= SyncWindow)
                    {
                        EmitReferenceOnly(_referenceEntryBacklog[0]);
                        _referenceEntryBacklog.RemoveAt(0);
                    }

                    _referenceEntryBacklog.Add(referenceEntry);
                }
                else
                {
                    EmitReferenceOnly(referenceEntry);
                }
            }
        }

        private void InitComparer()
        {
            if (_comparer != null)
                return;

            List<PSObject> referenceObjectList = new(ReferenceObject);
            _orderByProperty = new OrderByProperty(
                this, referenceObjectList, Property, true, _cultureInfo, CaseSensitive);
            Diagnostics.Assert(_orderByProperty.Comparer != null, "no comparer");
            Diagnostics.Assert(
                _orderByProperty.OrderMatrix != null &&
                _orderByProperty.OrderMatrix.Count == ReferenceObject.Length,
                "no OrderMatrix");
            if (_orderByProperty.Comparer == null || _orderByProperty.OrderMatrix == null || _orderByProperty.OrderMatrix.Count == 0)
            {
                return;
            }

            _comparer = _orderByProperty.Comparer;
            _referenceEntries = _orderByProperty.OrderMatrix;
        }

        private OrderByPropertyEntry MatchAndRemove(
            OrderByPropertyEntry match,
            List<OrderByPropertyEntry> list)
        {
            if (match == null || list == null)
                return null;
            Diagnostics.Assert(_comparer != null, "null comparer");
            for (int i = 0; i < list.Count; i++)
            {
                OrderByPropertyEntry listEntry = list[i];
                Diagnostics.Assert(listEntry != null, "null listEntry " + i);
                if (_comparer.Compare(match, listEntry) == 0)
                {
                    list.RemoveAt(i);
                    return listEntry;
                }
            }

            return null;
        }

        #region Emit
        private void EmitMatch(OrderByPropertyEntry entry)
        {
            if (_includeEqual)
                Emit(entry, SideIndicatorMatch);
        }

        private void EmitDifferenceOnly(OrderByPropertyEntry entry)
        {
            if (!ExcludeDifferent)
                Emit(entry, SideIndicatorDifference);
        }

        private void EmitReferenceOnly(OrderByPropertyEntry entry)
        {
            if (!ExcludeDifferent)
                Emit(entry, SideIndicatorReference);
        }

        private void Emit(OrderByPropertyEntry entry, string sideIndicator)
        {
            Diagnostics.Assert(entry != null, "null entry");

            PSObject mshobj;
            if (PassThru)
            {
                mshobj = PSObject.AsPSObject(entry.inputObject);
            }
            else
            {
                mshobj = new PSObject();
                if (Property == null || Property.Length == 0)
                {
                    PSNoteProperty inputNote = new(
                        InputObjectPropertyName, entry.inputObject);
                    mshobj.Properties.Add(inputNote);
                }
                else
                {
                    List<MshParameter> mshParameterList = _orderByProperty.MshParameterList;
                    Diagnostics.Assert(mshParameterList != null, "null mshParameterList");
                    Diagnostics.Assert(mshParameterList.Count == Property.Length, "mshParameterList.Count " + mshParameterList.Count);

                    for (int i = 0; i < Property.Length; i++)
                    {
                        // 2005/07/05 This is the closest we can come to
                        // the string typed by the user
                        MshParameter mshParameter = mshParameterList[i];
                        Diagnostics.Assert(mshParameter != null, "null mshParameter");
                        Hashtable hash = mshParameter.hash;
                        Diagnostics.Assert(hash != null, "null hash");
                        object prop = hash[FormatParameterDefinitionKeys.ExpressionEntryKey];
                        Diagnostics.Assert(prop != null, "null prop");
                        string propName = prop.ToString();
                        PSNoteProperty propertyNote = new(
                            propName,
                            entry.orderValues[i].PropertyValue);
                        try
                        {
                            mshobj.Properties.Add(propertyNote);
                        }
                        catch (ExtendedTypeSystemException)
                        {
                            // this is probably a duplicate add
                        }
                    }
                }
            }

            mshobj.Properties.Remove(SideIndicatorPropertyName);
            PSNoteProperty sideNote = new(
                SideIndicatorPropertyName, sideIndicator);
            mshobj.Properties.Add(sideNote);
            WriteObject(mshobj);
        }
        #endregion Emit
        #endregion Internal

        #region Overrides

        /// <summary>
        /// If the parameter 'ExcludeDifferent' is present, then the 'IncludeEqual'
        /// switch is turned on unless it's turned off by the user specifically.
        /// </summary>
        protected override void BeginProcessing()
        {
            if (ExcludeDifferent)
            {
                if (_isIncludeEqualSpecified && !_includeEqual)
                {
                    return;
                }

                _includeEqual = true;
            }
        }

        /// <summary>
        /// </summary>
        protected override void ProcessRecord()
        {
            if (ReferenceObject == null || ReferenceObject.Length == 0)
            {
                HandleDifferenceObjectOnly();
                return;
            }
            else if (DifferenceObject == null || DifferenceObject.Length == 0)
            {
                HandleReferenceObjectOnly();
                return;
            }

            if (_comparer == null && DifferenceObject.Length > 0)
            {
                InitComparer();
            }

            List<PSObject> differenceList = new(DifferenceObject);
            List<OrderByPropertyEntry> differenceEntries =
                OrderByProperty.CreateOrderMatrix(
                this, differenceList, _orderByProperty.MshParameterList);

            foreach (OrderByPropertyEntry incomingEntry in differenceEntries)
            {
                Process(incomingEntry);
            }
        }

        /// <summary>
        /// </summary>
        protected override void EndProcessing()
        {
            // Clear remaining reference objects if there are more
            // reference objects than difference objects
            if (_referenceEntries != null)
            {
                while (_referenceObjectIndex < _referenceEntries.Count)
                {
                    Process(null);
                }
            }

            // emit all remaining backlogged objects
            foreach (OrderByPropertyEntry differenceEntry in _differenceEntryBacklog)
            {
                EmitDifferenceOnly(differenceEntry);
            }

            _differenceEntryBacklog.Clear();
            foreach (OrderByPropertyEntry referenceEntry in _referenceEntryBacklog)
            {
                EmitReferenceOnly(referenceEntry);
            }

            _referenceEntryBacklog.Clear();
        }
        #endregion Overrides

        private void HandleDifferenceObjectOnly()
        {
            if (DifferenceObject == null || DifferenceObject.Length == 0)
            {
                return;
            }

            List<PSObject> differenceList = new(DifferenceObject);
            _orderByProperty = new OrderByProperty(
                this, differenceList, Property, true, _cultureInfo, CaseSensitive);
            List<OrderByPropertyEntry> differenceEntries =
                OrderByProperty.CreateOrderMatrix(
                this, differenceList, _orderByProperty.MshParameterList);

            foreach (OrderByPropertyEntry entry in differenceEntries)
            {
                EmitDifferenceOnly(entry);
            }
        }

        private void HandleReferenceObjectOnly()
        {
            if (ReferenceObject == null || ReferenceObject.Length == 0)
            {
                return;
            }

            InitComparer();
            Process(null);
        }
    }
}
