/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/
using System; using Microsoft;


#if !SILVERLIGHT // ComObject

using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using ComTypes = System.Runtime.InteropServices.ComTypes;

#if CODEPLEX_40
namespace System.Dynamic {
#else
namespace Microsoft.Scripting {
#endif
    /// <summary>
    /// This class implements an event sink for a particular RCW.
    /// Unlike the implementation of events in TlbImp'd assemblies,
    /// we will create only one event sink per RCW (theoretically RCW might have
    /// several ComEventSink evenk sinks - but all these implement different source intefaces).
    /// Each ComEventSink contains a list of ComEventSinkMethod objects - which represent
    /// a single method on the source interface an a multicast delegate to redirect 
    /// the calls. Notice that we are chaining multicast delegates so that same 
    /// ComEventSinkMedhod can invoke multiple event handlers).
    /// 
    /// ComEventSink implements an IDisposable pattern to Unadvise from the connection point.
    /// Typically, when RCW is finalized the corresponding Dispose will be triggered by 
    /// ComEventSinksContainer finalizer. Notice that lifetime of ComEventSinksContainer
    /// is bound to the lifetime of the RCW. 
    /// </summary>
    internal sealed class ComEventSink : MarshalByRefObject, IReflect, IDisposable {

        #region private fields

        private Guid _sourceIid;
        private ComTypes.IConnectionPoint _connectionPoint;
        private int _adviseCookie;
        private List<ComEventSinkMethod> _comEventSinkMethods;
        private object _lockObject = new object(); // We cannot lock on ComEventSink since it causes a DoNotLockOnObjectsWithWeakIdentity warning

        #endregion

        #region private classes

        /// <summary>
        /// Contains a methods DISPID (in a string formatted of "[DISPID=N]"
        /// and a chained list of delegates to invoke
        /// </summary>
        private class ComEventSinkMethod {
            public string _name;
            public Func<object[], object> _handlers;
        }
        #endregion

        #region ctor

        [SecurityCritical]
        private ComEventSink(object rcw, Guid sourceIid) {
            Initialize(rcw, sourceIid);
        }

        #endregion

        [SecurityCritical]
        private void Initialize(object rcw, Guid sourceIid) {
            _sourceIid = sourceIid;
            _adviseCookie = -1;

            Debug.Assert(_connectionPoint == null, "re-initializing event sink w/o unadvising from connection point");

            ComTypes.IConnectionPointContainer cpc = rcw as ComTypes.IConnectionPointContainer;
            if (cpc == null)
                throw Error.COMObjectDoesNotSupportEvents();

            cpc.FindConnectionPoint(ref _sourceIid, out _connectionPoint);
            if (_connectionPoint == null)
                throw Error.COMObjectDoesNotSupportSourceInterface();

            // Read the comments for ComEventSinkProxy about why we need it
            ComEventSinkProxy proxy = new ComEventSinkProxy(this, _sourceIid);
            _connectionPoint.Advise(proxy.GetTransparentProxy(), out _adviseCookie);
        }

        #region static methods

        [SecurityCritical]
        public static ComEventSink FromRuntimeCallableWrapper(object rcw, Guid sourceIid, bool createIfNotFound) {
            List<ComEventSink> comEventSinks = ComEventSinksContainer.FromRuntimeCallableWrapper(rcw, createIfNotFound);

            if (comEventSinks == null) {
                return null;
            }

            ComEventSink comEventSink = null;
            lock (comEventSinks) {

                foreach (ComEventSink sink in comEventSinks) {
                    if (sink._sourceIid == sourceIid) {
                        comEventSink = sink;
                        break;
                    } else if (sink._sourceIid == Guid.Empty) {
                        // we found a ComEventSink object that 
                        // was previously disposed. Now we will reuse it.
                        sink.Initialize(rcw, sourceIid);
                        comEventSink = sink;
                    }
                }

                if (comEventSink == null && createIfNotFound == true) {
                    comEventSink = new ComEventSink(rcw, sourceIid);
                    comEventSinks.Add(comEventSink);
                }
            }

            return comEventSink;
        }

        #endregion

        public void AddHandler(int dispid, object func) {
            string name = String.Format(CultureInfo.InvariantCulture, "[DISPID={0}]", dispid);

            lock (_lockObject) {
                ComEventSinkMethod sinkMethod;
                sinkMethod = FindSinkMethod(name);

                if (sinkMethod == null) {
                    if (_comEventSinkMethods == null) {
                        _comEventSinkMethods = new List<ComEventSinkMethod>();
                    }

                    sinkMethod = new ComEventSinkMethod();
                    sinkMethod._name = name;
                    _comEventSinkMethods.Add(sinkMethod);
                }

                sinkMethod._handlers += new SplatCallSite(func).Invoke;
            }
        }

        [SecurityCritical]
        public void RemoveHandler(int dispid, object func) {

            string name = String.Format(CultureInfo.InvariantCulture, "[DISPID={0}]", dispid);

            lock (_lockObject) {

                ComEventSinkMethod sinkEntry = FindSinkMethod(name);
                if (sinkEntry == null){
                    return;
                }

                // Remove the delegate from multicast delegate chain.
                // We will need to find the delegate that corresponds
                // to the func handler we want to remove. This will be
                // easy since we Target property of the delegate object
                // is a ComEventCallContext object.
                Delegate[] delegates = sinkEntry._handlers.GetInvocationList();
                foreach (Delegate d in delegates) {
                    SplatCallSite callContext = d.Target as SplatCallSite;
                    if (callContext != null && callContext._callable.Equals(func)) {
                        sinkEntry._handlers -= d as Func<object[], object>;
                        break;
                    }
                }

                // If the delegates chain is empty - we can remove 
                // corresponding ComEvenSinkEntry
                if (sinkEntry._handlers == null)
                    _comEventSinkMethods.Remove(sinkEntry);

                // We can Unadvise from the ConnectionPoint if no more sink entries
                // are registered for this interface 
                //(calling Dispose will call IConnectionPoint.Unadvise).
                if (_comEventSinkMethods.Count == 0) {
                    // notice that we do not remove 
                    // ComEventSinkEntry from the list, we will re-use this data structure
                    // if a new handler needs to be attached.
                    Dispose();
                }
            }
        }

        public object ExecuteHandler(string name, object[] args) {
            ComEventSinkMethod site;
            site = FindSinkMethod(name);

            if (site != null && site._handlers != null) {
                return site._handlers(args);
            }

            return null;
        }

        #region IReflect

        #region Unimplemented members

        public FieldInfo GetField(string name, BindingFlags bindingAttr) {
            return null;
        }

        public FieldInfo[] GetFields(BindingFlags bindingAttr) {
            return new FieldInfo[0];
        }

        public MemberInfo[] GetMember(string name, BindingFlags bindingAttr) {
            return new MemberInfo[0];
        }

        public MemberInfo[] GetMembers(BindingFlags bindingAttr) {
            return new MemberInfo[0];
        }

        public MethodInfo GetMethod(string name, BindingFlags bindingAttr) {
            return null;
        }

        public MethodInfo GetMethod(string name, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers) {
            return null;
        }

        public MethodInfo[] GetMethods(BindingFlags bindingAttr) {
            return new MethodInfo[0];
        }

        public PropertyInfo GetProperty(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, ParameterModifier[] modifiers) {
            return null;
        }

        public PropertyInfo GetProperty(string name, BindingFlags bindingAttr) {
            return null;
        }

        public PropertyInfo[] GetProperties(BindingFlags bindingAttr) {
            return new PropertyInfo[0];
        }

        #endregion

        public Type UnderlyingSystemType {
            get {
                return typeof(object);
            }
        }

        public object InvokeMember(
            string name,
            BindingFlags invokeAttr,
            Binder binder,
            object target,
            object[] args,
            ParameterModifier[] modifiers,
            CultureInfo culture,
            string[] namedParameters) {

            return ExecuteHandler(name, args);
        }

        #endregion

        #region IDisposable

#if CLR2
        [SecurityCritical, SecurityTreatAsSafe]
#else
        [SecuritySafeCritical]
#endif
        public void Dispose() {
            DisposeAll();
            GC.SuppressFinalize(this);
        }

        #endregion

#if CLR2
        [SecurityCritical, SecurityTreatAsSafe]
#else
        [SecuritySafeCritical]
#endif
        ~ComEventSink() {
            DisposeAll();
        }

        [SecurityCritical]
        private void DisposeAll() {
            if (_connectionPoint == null) {
                return;
            }

            if (_adviseCookie == -1) {
                return;
            }

            try {
                _connectionPoint.Unadvise(_adviseCookie);

                // _connectionPoint has entered the CLR in the constructor
                // for this object and hence its ref counter has been increased
                // by us. We have not exposed it to other components and
                // hence it is safe to call RCO on it w/o worrying about
                // killing the RCW for other objects that link to it.
                Marshal.ReleaseComObject(_connectionPoint);
            } catch (Exception ex) {
                // if something has gone wrong, and the object is no longer attached to the CLR,
                // the Unadvise is going to throw.  In this case, since we're going away anyway,
                // we'll ignore the failure and quietly go on our merry way.
                COMException exCOM = ex as COMException;
                if (exCOM != null && exCOM.ErrorCode == ComHresults.CONNECT_E_NOCONNECTION) {
                    Debug.Assert(false, "IConnectionPoint::Unadvise returned CONNECT_E_NOCONNECTION.");
                    throw;
                }
            } finally {
                _connectionPoint = null;
                _adviseCookie = -1;
                _sourceIid = Guid.Empty;
            }
        }

        private ComEventSinkMethod FindSinkMethod(string name) {
            if (_comEventSinkMethods == null)
                return null;

            ComEventSinkMethod site;
            site = _comEventSinkMethods.Find(element => element._name == name);

            return site;
        }
    }
}

#endif