/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;

namespace Apache.Geode.Client.FwkClient
{
  using Apache.Geode.DUnitFramework;

  class ClientComm : MarshalByRefObject, IClientComm
  {
    private static Dictionary<string, object> m_InstanceMap =
      new Dictionary<string, object>();
    private static Dictionary<string, AppDomain> m_appDomainMap =
      new Dictionary<string, AppDomain>();
    private static volatile bool m_exiting = false;

    private void CallP(int objectId, string assemblyName, string typeName,
      string methodName, object[] paramList, ref object result)
    {
      if (m_exiting) {
        return;
      }
      string objID = objectId.ToString();
      object typeInst = null;
      string appDomain = null;
      // assume first argument to be the name of AppDomain
      if (paramList != null && paramList.Length > 0) {
        object param = paramList[paramList.Length - 1];
        if (param is string && (appDomain = (string)param).StartsWith(
            Util.AppDomainPrefix)) {
          appDomain = appDomain.Substring(Util.AppDomainPrefix.Length);
          if (appDomain.Length > 0) {
            objID = Util.AppDomainPrefix + ":" + appDomain + objID;
            object[] newParams = new object[paramList.Length - 1];
            Array.Copy(paramList, 0, newParams, 0, paramList.Length - 1);
            paramList = newParams;
          }
          else {
            appDomain = null;
          }
        }
        else {
          appDomain = null;
        }
      }
      lock (((ICollection)m_InstanceMap).SyncRoot) {
        if (m_InstanceMap.ContainsKey(objID)) {
          typeInst = m_InstanceMap[objID];
        }
        else if (appDomain != null)
        {
          AppDomain domain;
          if (!m_appDomainMap.ContainsKey(appDomain)) {
            domain = AppDomain.CreateDomain(appDomain);
            CreateBBServerConnection urlConn = (CreateBBServerConnection)domain.
              CreateInstanceAndUnwrap("FwkClient",
              "Apache.Geode.Client.FwkClient.CreateBBServerConnection");
            urlConn.OpenBBServerConnection(ClientProcess.bbUrl);
            urlConn.SetLogFile(Util.LogFile);
            Util.Log("Created app domain {0}",domain);
            m_appDomainMap.Add(appDomain, domain);
          }
          else {
            domain = m_appDomainMap[appDomain];
            CreateBBServerConnection urlConn = (CreateBBServerConnection)domain.
              CreateInstanceAndUnwrap("FwkClient",
              "Apache.Geode.Client.FwkClient.CreateBBServerConnection");
            urlConn.SetLogFile(Util.LogFile);
          }
          try {
            typeInst = domain.CreateInstanceAndUnwrap(assemblyName, typeName);
          } catch (Exception e) {
            Util.Log("Exception thrown for app domain CreateInstanceAndUnwrap : {0} ", e.Message);
          }
          if (typeInst != null) {
            m_InstanceMap[objID] = typeInst;
          }
          else {
            throw new IllegalArgException(
              "FATAL: Could not load class with name: " + typeName);
          }
        }
        else {
          Assembly assmb = null;
          try {
            assmb = Assembly.Load(assemblyName);
          } catch (Exception) {
            throw new IllegalArgException(
              "FATAL: Could not load assembly: " + assemblyName);
          }
          typeInst = assmb.CreateInstance(typeName);
          if (typeInst != null) {
            m_InstanceMap[objID] = typeInst;
          }
          else {
            throw new IllegalArgException(
              "FATAL: Could not load class with name: " + typeName);
          }
        }
      }
      MethodInfo[] mInfos = typeInst.GetType().GetMethods(
        BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public
        | BindingFlags.NonPublic | BindingFlags.Static
        | BindingFlags.FlattenHierarchy);
      MethodInfo mInfo = null;
      foreach (MethodInfo method in mInfos) {
        if (method.Name.Equals(methodName,
          StringComparison.CurrentCultureIgnoreCase)) {
          ParameterInfo[] prms = method.GetParameters();
          int paramLen = (paramList != null ? paramList.Length : 0);
          if (paramLen == prms.Length) {
            mInfo = method;
          }
        }
      }
      if (mInfo != null) {
        Thread currentThread = Thread.CurrentThread;
        lock (((ICollection)UnitThread.GlobalUnitThreads).SyncRoot) {
          if (!UnitThread.GlobalUnitThreads.ContainsKey(currentThread)) {
            UnitThread.GlobalUnitThreads.Add(currentThread, true);
          }
        }
        if (result == null) {
          try
          {
            mInfo.Invoke(typeInst, paramList);
          }
          catch (TargetInvocationException tie)
          {
            Util.Log(Util.LogLevel.Error, tie.InnerException.StackTrace.ToString());
            throw tie;
          }
        }
        else {
          result = mInfo.Invoke(typeInst, paramList);
        }
        lock (((ICollection)UnitThread.GlobalUnitThreads).SyncRoot) {
          if (!UnitThread.GlobalUnitThreads.ContainsKey(currentThread)) {
            UnitThread.GlobalUnitThreads.Remove(currentThread);
          }
        }
      }
      else {
        throw new IllegalArgException("FATAL: Could not load function with name: " +
          methodName + ", in class: " + typeInst.GetType());
      }
    }

    #region IClientComm Members

    public void Call(int objectId, string assemblyName, string typeName,
      string methodName, params object[] paramList)
    {
      object obj = null;
      CallP(objectId, assemblyName, typeName, methodName, paramList, ref obj);
    }

    public object CallR(int objectId, string assemblyName, string typeName,
      string methodName, params object[] paramList)
    {
      object obj = 0;
      CallP(objectId, assemblyName, typeName, methodName, paramList, ref obj);
      return obj;
    }

    public void RemoveObjectID(int objectId)
    {
      lock (((ICollection)m_InstanceMap).SyncRoot)
      {
        if (m_InstanceMap.ContainsKey(objectId.ToString()))
        {
          m_InstanceMap.Remove(objectId.ToString());
        }
      }
    }

    public void SetLogFile(string logFile)
    {
      Util.LogFile = logFile;
    }

    public void SetLogLevel(Util.LogLevel logLevel)
    {
      Util.CurrentLogLevel = logLevel;
    }

    public bool CreateNew(string clientId, int port)
    {
      string clientPath = Assembly.GetExecutingAssembly().Location;
      string[] args = Environment.GetCommandLineArgs();
      string argStr = string.Empty;
      for (int i = 1; i < args.Length - 1; i++)
      {
        string arg = args[i];
        if (arg.StartsWith("--id="))
        {
          argStr += ("\"--id=" + clientId + "\" ");
        }
        else
        {
          argStr += ('"' + arg + "\" ");
        }
      }
      argStr += port.ToString();
      Process proc;
      return Util.StartProcess(clientPath, argStr, false, null,
        false, false, false, out proc);
    }

    public void DumpStackTrace()
    {
      StringBuilder dumpStr = new StringBuilder();
      try
      {
        dumpStr.Append(Util.MarkerString);
        dumpStr.Append("Dumping managed stack for process [" + Util.PID + "]:");
        dumpStr.Append(Util.MarkerString);
        lock (((ICollection)UnitThread.GlobalUnitThreads).SyncRoot)
        {
          foreach (Thread thread in UnitThread.GlobalUnitThreads.Keys)
          {
            dumpStr.Append(UnitThread.GetStackTrace(thread));
            dumpStr.Append(Environment.NewLine + Environment.NewLine);
          }
        }
        dumpStr.Append(Util.MarkerString);
        Util.Log(dumpStr.ToString());
        dumpStr.Length = 0;
        dumpStr.Append("Dumping native stack for process [" + Util.PID + "]:");
        dumpStr.Append(Util.MarkerString);
        Process cdbProc;
        // Expect to find 'cdb.pl' in the same dir as FwkClient.exe
        if (Util.StartProcess("perl", Util.AssemblyDir + "/cdb.pl " + Util.PID,
          false, null, true, false, true, out cdbProc))
        {
          StreamReader cdbRead = cdbProc.StandardOutput;
          StreamReader cdbReadErr = cdbProc.StandardError;
          dumpStr.Append(cdbRead.ReadToEnd());
          dumpStr.Append(cdbReadErr.ReadToEnd());
          cdbProc.WaitForExit();
          cdbRead.Close();
          cdbReadErr.Close();
        }
        else
        {
          dumpStr.Append("Failed to start: perl ");
          dumpStr.Append(Util.AssemblyDir + "/cdb.pl " + Util.PID);
        }
      }
      catch (Exception ex)
      {
        dumpStr.Append("Exception while invoking cdb.pl: ");
        dumpStr.Append(ex.ToString());
      }
      dumpStr.Append(Util.MarkerString + Environment.NewLine);
      Util.Log(dumpStr.ToString());
    }

    public void TestCleanup()
    {
      foreach (UnitFnMethod deleg in Util.RegisteredTestCompleteDelegates)
      {
        try
        {
          Util.Log("Cleaning test on client [{0}]: Calling {1} :: {2}",
            Util.ClientId, deleg.Method.DeclaringType, deleg.Method);
          deleg();
        }
        catch (Exception ex)
        {
          Util.Log(Util.LogLevel.Error, ex.ToString());
        }
      }
    }

    public void Exit(bool force)
    {
      try
      {
        m_exiting = true;
        if (!force)
        {
          foreach (UnitFnMethod deleg in Util.RegisteredTestCompleteDelegates)
          {
            try
            {
              Util.Log("Exiting client [{0}]: Calling {1} :: {2}", Util.ClientId,
                deleg.Method.DeclaringType, deleg.Method);
              deleg();
            }
            catch (Exception ex)
            {
              Util.Log(Util.LogLevel.Error, ex.ToString());
            }
          }
        }
      }
      finally
      {
        Environment.Exit(0);
        Util.Process.Kill();
      }
    }

    public Process GetClientProcess()
    {
      return Util.Process;
    }

    #endregion
  }
}
