/*******************************************************************************

Copyright (c) 2010, Perforce Software, Inc.  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1.  Redistributions of source code must retain the above copyright
	notice, this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
	notice, this list of conditions and the following disclaimer in the
	documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*******************************************************************************/

/*******************************************************************************
 * Name		: P4Server.cs
 *
 * Author	: dbb
 *
 * Description	: Classes used to wrap calls in the P4Bridge DLL in C#.
 *
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
using System.Timers;
using System.Threading;

namespace Perforce.P4
{
    /// <summary>
    /// Allows a client to monitor the execution of a command. It allow the client to
    /// cancel the command if it takes to long to complete or display a UI to allow the
    /// user to cancel the command.
    /// </summary>
    public interface IKeepAlive
    {
        /// <summary>
        /// A command is starting
        /// </summary>
        /// <param name="server">Server running the command</param>
        /// <param name="cmdId">CmdId of the command</param>
        /// <param name="cmdRunThread">Thread to run on</param>
        /// <param name="cmdLine">Command Line for the command for display purposes</param>
        /// <returns></returns>
        bool StartQueryCancel(P4Server server, uint cmdId, Thread cmdRunThread, string cmdLine);
        // Note, it is OK for the API might send this multiple times with the,
        // same cmdId to ensure that any UI displayed by the client is dismissed.
        /// <summary>
        /// The command has completed dismiss any UI or timeouts
        /// </summary>
        /// <param name="cmdId">CmdId of the command that completed</param>
        /// <remarks>
        /// Note, it is OK for the API might send this multiple times with the,
        /// same cmdId to ensure that any UI displayed by the client iss dismissed.
        /// </remarks>
        void CommandCompleted(uint cmdId);
    }

    /// <summary>
    /// P4Server
    ///
    /// Represents the connection to a P4 Server using the the P4 Bridge
    /// DLL. It wraps the calls exported by the DLL and transforms the data
    /// types exported by the DLL as native C#.NET data types.
    /// </summary>
    public partial class P4Server : IDisposable
    {
        public IKeepAlive KeepAlive { get; set; }

        internal object Sync = new object();

        private Dictionary<int, P4CommandResult> _lastResultsCache;
        private readonly object _lastResultsCacheLock = new object();
        /// <summary>
        /// The results of the last command executed on this thread
        /// </summary>
        public P4CommandResult LastResults
        {
            get
            {
                int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

                lock (_lastResultsCacheLock)
                {
                    if (_lastResultsCache != null && _lastResultsCache.TryGetValue(threadId, out var result))
                    {
                        return result;
                    }
                    return null;
                }
            }
            internal set
            {
                int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
                lock (_lastResultsCacheLock)
                {
                    if (_lastResultsCache == null)
                    {
                        _lastResultsCache = new Dictionary<int, P4CommandResult>();
                    }
                    if (_lastResultsCache.Count > 32)
                    {
                        // if the results cache is getting large, throw away anything older than 10 seconds
                        int[] keys = new int[_lastResultsCache.Keys.Count];
                        _lastResultsCache.Keys.CopyTo(keys, 0);
                        DateTime old = DateTime.Now - TimeSpan.FromSeconds(10);
                        foreach (int key in keys)
                        {
                            if (_lastResultsCache[key].TimeStamp < old)
                            {
                                Debug.Trace(string.Format("Throwing away results for thread, {0}", key));
                                _lastResultsCache.Remove(key);
                            }
                        }
                    }
                    _lastResultsCache[threadId] = value;
                }
            }
        }

        /// <summary>
        /// Get the version of the p4.net assembly
        /// </summary>
        public static System.Version Version
        {
            get
            {
                return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
            }
        }

        /// <summary>
        /// Get the error message generated by the previous connection (if any)
        /// </summary>
        public static P4ClientError ConnectionErrorInt
        {
            get
            {
                IntPtr pErr = P4Bridge.GetConnectionError();
                if (pErr != IntPtr.Zero)
                {
                    P4ClientError pRet = new P4ClientError(null, pErr);
                    P4Bridge.ClearConnectionError();
                    return pRet;
                }
                return null;
            }
        }

        /// <summary>
        /// Get the error message generated by the previous connection (if any)
        /// from the bridge dll.
        /// </summary>
        public P4ClientError ConnectionError { get; private set; }


        private static void LogBridgeMessage(int log_level,
                                        String file,
                                        int line,
                                        String message)
        {
            Debug.Trace(String.Format("{0}:{1} {2}", file, line, message));

            // remove the full path to the source, keep just the file name
            String fileName = Path.GetFileName(file);

            string category = String.Format("P4Bridge({0}:{1})", fileName, line);

            LogFile.LogMessage(log_level, category, message);
        }

        private string _server;
        private string _user;
        private string _pass;
        private string _ws_client;
        private string _prog_name;
        private string _prog_ver;
        private string _cwd;

        private static P4CallBacks.LogMessageDelegate logfn = new P4CallBacks.LogMessageDelegate(LogBridgeMessage);
        private static IntPtr pLogFn = IntPtr.Zero; // contains pinned pointer for the bridge
                                                    // P4Server.cs (inside P4Server class)

        /// <summary>
        /// Progress callback delegates and registration.
        /// Used to receive progress events from the bridge while running commands.
        /// </summary>
        public ProgressHandler Progress { get; set; }
        private P4CallBacks.ProgressInitCallback progressInitCallback;
        private P4CallBacks.ProgressDescriptionCallback progressDescriptionCallback;
        private P4CallBacks.ProgressTotalCallback progressTotalCallback;
        private P4CallBacks.ProgressUpdateCallback progressUpdateCallback;
        private P4CallBacks.ProgressDoneCallback progressDoneCallback;

        public void SetProgressCallbacks()
        {
            if (Progress != null)
            {
                progressInitCallback = Progress.Init;
                progressDescriptionCallback = Progress.Description;
                progressTotalCallback = Progress.Total;
                progressUpdateCallback = Progress.Update;
                progressDoneCallback = Progress.Done;

                P4Bridge.SetProgressCallbacks(
                    pServer,
                    Progress.Init != null ? Marshal.GetFunctionPointerForDelegate(Progress.Init) : IntPtr.Zero,
                    Progress.Description != null ? Marshal.GetFunctionPointerForDelegate(Progress.Description) : IntPtr.Zero,
                    Progress.Total != null ? Marshal.GetFunctionPointerForDelegate(Progress.Total) : IntPtr.Zero,
                    Progress.Update != null ? Marshal.GetFunctionPointerForDelegate(Progress.Update) : IntPtr.Zero,
                    Progress.Done != null ? Marshal.GetFunctionPointerForDelegate(Progress.Done) : IntPtr.Zero);
            }
        }
        /// <summary>
        /// Set the progress handler for reporting progress events.
        /// </summary>
        public void SetProgressHandler(ProgressHandler handler)
        {
            Progress = handler;
        }

        /// <summary>
        /// Reset the progress handler.
        /// </summary>
        public void ResetProgressHandler()
        {
            Progress = null;
        }

        /// <summary>
        /// Create a P4BridgeServer used to connect to the specified P4Server
        /// </summary>
        /// <param name="server">Host:port for the P4 server.</param>
        /// <param name="user">User name for the login.
        ///     Can be null/blank if only running commands that do not require
        ///     a login.</param>
        /// <param name="pass">Password for  the login. Can be null/blank if
        ///     only running commands that do not require a login.</param>
        /// <param name="ws_client">Workspace (client) to be used by the
        ///     connection. Can be null/blank if only running commands that do
        ///     not require a login.</param>
        public P4Server(String server,
                         String user,
                         String pass,
                         String ws_client)
            : this(server, user, pass, ws_client, null)
        {
        }

        /// <summary>
        /// Create a P4BridgeServer using the PUC specified by the environment
        ///		or a p4config file if one exists.
        /// </summary>
        /// <param name="cwd">Current working Directory. Can be null/blank if
        ///		not connecting to the P4 Server using a P4Config file.</param>
        public P4Server(String cwd)
            : this(null, null, null, null, cwd)
        {
        }
        /// <summary>
        /// Create a P4BridgeServer used to connect to the specified P4Server
        /// </summary>
        /// <param name="server">Host:port for the P4 server.</param>
        /// <param name="user">User name for the login.
        ///     Can be null/blank if only running commands that do not require
        ///     a login.</param>
        /// <param name="pass">Password for  the login. Can be null/blank if
        ///     only running commands that do not require a login.</param>
        /// <param name="ws_client">Workspace (client) to be used by the
        ///     connection. Can be null/blank if only running commands that do
        ///     not require a login.</param>
        /// <param name="cwd">Current working Directory. Can be null/blank if
        ///		not connecting to the P4 Server using a P4Config file.</param>
        internal P4Server(String server,
                         String user,
                         String pass,
                         String ws_client,
                         String cwd)
        {
            RunCmdLastContactMap = new Dictionary<uint, CmdContactTimer>();
            // by default we are owned by the creating thread
            SetThreadOwner(Thread.CurrentThread.ManagedThreadId);

            if (string.IsNullOrEmpty(cwd) == false) // use P4CONFIG to fill out missing settings
            {
                string tmpServer, tmpUser, tmpClient;
                ConnectionInfoFromPath(cwd, out tmpServer, out tmpUser, out tmpClient);
                server = (server == null) ? tmpServer : server;
                user = (user == null) ? tmpUser : user;
                ws_client = (ws_client == null) ? tmpClient : ws_client;
            }
            _server = server;

            _user = user;
            _pass = pass;
            _ws_client = ws_client;
            _cwd = cwd;
#if _OSX || _LINUX
            // Enviro can sometimes miss environment variables when not in windows
            // So we check for them here also
            if (string.IsNullOrEmpty(_server))
            {
                string eport = Environment.GetEnvironmentVariable("P4PORT");
                if (!string.IsNullOrEmpty(eport))
                {
                    _server = server = eport;
                }
            }
            if (string.IsNullOrEmpty(_user))
            {
                string euser = Environment.GetEnvironmentVariable("P4USER");
                if (!string.IsNullOrEmpty(euser))
                {
                    _user = user = euser;
                }
            }
            if (string.IsNullOrEmpty(_ws_client))
            {
                string eclient = Environment.GetEnvironmentVariable("P4CLIENT");
                if (!string.IsNullOrEmpty(eclient))
                {
                    _ws_client = ws_client = eclient;
                }
            }
            if (string.IsNullOrEmpty(_pass))
            {
                string epass = Environment.GetEnvironmentVariable("P4PASSWD");
                if (!string.IsNullOrEmpty(epass))
                {
                    _pass = pass = epass;
                }
            }
#endif
            isUnicode = false;

            CurrentEncodeing = P4Encoding.ASCII;

            // connect to the server without passing user/client/password, that way
            // we can determine if the target server is Unicode enabled or not, so we
            // can use the correct encoding for those parameters

            pServer = P4Bridge.ConnectA(server, _user, null, null, logfn);

            if (pServer != IntPtr.Zero)
            {
                CurrentWorkingDirectory = cwd;  // set up the environ

                if (isUnicode = P4Bridge.IsUnicode(pServer))
                {
                    // if the server supports Unicode, encode the cwd, username and workspace
                    // name in UTF-8
                    using (PinnedByteArray pCwd = MarshalStringToIntPtr(cwd),
                                           pUser = MarshalStringToIntPtr(user),
                                           pClient = MarshalStringToIntPtr(ws_client))
                    {
                        P4Bridge.set_cwdW(pServer, pCwd);
                        P4Bridge.set_userW(pServer, pUser);
                        P4Bridge.set_clientW(pServer, pClient);
                    }
                }
                else
                {
                    // if the server does not support Unicode, pass the cwd, username and workspace
                    //  name in ASCII
                    P4Bridge.set_cwdA(pServer, cwd);
                    P4Bridge.set_userA(pServer, user);
                    P4Bridge.set_clientA(pServer, ws_client);
                }
            }
            if (pServer == IntPtr.Zero)
            {

                ConnectionError = ConnectionErrorInt;
                if (ConnectionError == null)
                {
                    IntPtr pObj = P4Bridge.GetErrorResults(pServer, 0);
                    P4ClientErrorList errorList = new P4ClientErrorList(this, pObj);

                    if (errorList.Count > 0)
                    {
                        P4Exception.Throw(errorList, GetInfoResults(0));
                    }
                    ConnectionError = new P4ClientError(ErrorSeverity.E_FATAL, string.Format("Unknown error connecting to server, {0}", _server));
                }
                P4Exception.Throw(ConnectionError);
                return;
            }
            else
            {
                ConnectionError = null;
            }


            if (isUnicode)
            {
                CurrentEncodeing = P4Encoding.utf8;

                string charset = CharacterSet;
                string gotCharset = P4Server.Get("P4CHARSET");
                if (charset.Equals("none") && gotCharset != null)
                {
                    charset = gotCharset;
                }
                else
                {
                    charset = "utf8";
                }

                SetCharacterSet(charset, "utf8");
            }
            apiLevel = P4Bridge.APILevel(pServer);
            requiresLogin = P4Bridge.UseLogin(pServer);

            if ((!requiresLogin) && (!string.IsNullOrEmpty(pass)))
            {
                using (PinnedByteArray pPass = MarshalStringToIntPtr(pass))
                {
                    P4Bridge.set_passwordW(pServer, pPass);
                }
            }
            // Link the callbacks from the bridge dll to their corresponding events
            SetInfoResultsCallback();
            SetTaggedOutputCallback();
            SetErrorCallback();
            SetTextResultsCallback();
            SetBinaryResultsCallback();
            SetPromptCallback();
            SetResolveCallback();
            SetResolveACallback();
            SetParallelTransferCallback();
        }

        /// <summary>
        /// Create a P4BridgeServer used to connect to the specified P4Server
        /// </summary>
        /// <param name="server">Host:port for the P4 server.</param>
        /// <param name="user">User name for the login.
        ///     Can be null/blank if only running commands that do not require
        ///     a login.</param>
        /// <param name="pass">Password for  the login. Can be null/blank if
        ///     only running commands that do not require a login.</param>
        /// <param name="ws_client">Workspace (client) to be used by the
        ///     connection. Can be null/blank if only running commands that do
        ///     not require a login.</param>
        /// <param name="cwd">Current working directory</param>
        /// <param name="trust_flag">Trust or not</param>
        /// <param name="fingerprint">Fingerprint to trust</param>
        public P4Server(String server,
                         String user,
                         String pass,
                         String ws_client,
                         String cwd,
                         String trust_flag,
                         String fingerprint)
        {
            RunCmdLastContactMap = new Dictionary<uint, CmdContactTimer>();

            _server = server;
            _user = user;
            _pass = pass;
            _ws_client = ws_client;
            _cwd = cwd;

            CurrentEncodeing = P4Encoding.ASCII;

            // encode the username, password, and workspace name in UTF-8, we
            // won't know if the client supports Unicode until after connect
            // returns
            using (PinnedByteArray pUser = MarshalStringToIntPtr(user),
                                   pPass = MarshalStringToIntPtr(pass),
                                   pClient = MarshalStringToIntPtr(ws_client))
            {
                if (logfn != null)
                    pLogFn = Marshal.GetFunctionPointerForDelegate(logfn);

                pServer = P4Bridge.TrustedConnectW(server, pUser, pPass, pClient, trust_flag, fingerprint, pLogFn);
            }

            if (pServer == IntPtr.Zero)
            {

                ConnectionError = ConnectionErrorInt;
                if (ConnectionError == null)
                {
                    IntPtr pObj = P4Bridge.GetErrorResults(pServer, 0);
                    P4ClientErrorList errorList = new P4ClientErrorList(this, pObj);

                    if (errorList.Count > 0)
                    {
                        P4Exception.Throw(errorList, GetInfoResults(0));
                    }
                    ConnectionError = new P4ClientError(ErrorSeverity.E_FAILED,
                        string.Format("Unknown error connecting to server, {0}", _server));
                }
                P4Exception.Throw(ConnectionError);
                return;
            }
            else
            {
                ConnectionError = null;
            }
            CurrentWorkingDirectory = cwd;  // set up the environ

            if (isUnicode = P4Bridge.IsUnicode(pServer))
            {
                CurrentEncodeing = P4Encoding.utf8;

                SetCharacterSet("utf8", "utf8");
            }
            apiLevel = P4Bridge.APILevel(pServer);
            requiresLogin = P4Bridge.UseLogin(pServer);

            if ((!requiresLogin) && (!string.IsNullOrEmpty(pass)))
            {
                using (PinnedByteArray pPass = MarshalStringToIntPtr(pass))
                {
                    P4Bridge.set_passwordW(pServer, pPass);
                }
            }
            // Link the callbacks from the bridge dll to their corresponding events
            SetInfoResultsCallback();
            SetTaggedOutputCallback();
            SetErrorCallback();
            SetTextResultsCallback();
            SetBinaryResultsCallback();
            SetPromptCallback();
            SetResolveCallback();
            SetResolveACallback();
            SetParallelTransferCallback();

            // If we were supplied a password, login into the server. If the
            // server requires a login (security level >= 3), this will prompt
            // for the password. If login is not required, the command will just
            // return with a result saying that login is not required.
            //Login(pass, null);
        }

        /// <summary>
        /// Run a login command on the server
        /// </summary>
        /// <param name="password">User's password</param>
        /// <param name="options">options/flags</param>
        /// <returns>Success/Failure</returns>
        /// <remarks>
        /// If the server requires a login (security level >= 3), this will
        /// prompt for the password. If login is not required, the command will
        /// just return with a result saying that login is not required.
        /// </remarks>
        public bool Login(string password, StringList options)
        {
            Password = password;
            if (!requiresLogin)
            {
                // server does not support login command
                if (!string.IsNullOrEmpty(password))
                {
                    using (PinnedByteArray pPass = MarshalStringToIntPtr(password))
                    {
                        P4Bridge.set_passwordW(pServer, pPass);
                    }
                    return true;
                }
                return false;
            }
            using (P4Command login = new P4Command(this, "login", false))
            {
                login.Responses = new Dictionary<string, string>();
                login.Responses["DefaultResponse"] = password;
                P4CommandResult results;
                try
                {
                    results = login.Run(options);
                }
                catch
                {
                    results = null;
                    return false;
                }

                if (!results.Success)
                {
                    ConnectionError = results.ErrorList[0];
                }
                else
                {
                    ConnectionError = null;
                }

                return results.Success;
            }
        }

        /// <summary>
        /// Run a logout command on the server
        /// </summary>
        /// <param name="options">The -a flag invalidates the ticket on the server.</param>
        /// <param name="user">The user to log out (requires super access)</param>
        /// <returns>Success/Failure</returns>
        /// <remarks>
        /// If the server requires a login (security level >= 3), this will
        /// logout the user and remove the local ticket.
        /// </remarks>
        public bool Logout(StringList options, string user = null)
        {
            using (P4Command logout = user != null ?
                new P4Command(this, "logout", false, user) :
                new P4Command(this, "logout", false))
            {
                P4CommandResult results;
                try
                {
                    results = logout.Run(options);
                }
                catch
                {
                    return false;
                }

                if (!results.Success)
                {
                    ConnectionError = results.ErrorList[0];
                }
                else
                {
                    ConnectionError = null;
                }

                return results.Success;
            }
        }

        /// <summary>
        /// Finalizer
        /// </summary>
        ~P4Server() { Dispose(false); }

        bool _disposed = false;

        public void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            if (!disposing)
            {
            }
            try
            {
                Close();

                TaggedOutputCallbackFn_Int = null;
                pTaggedOutputCallbackFn = IntPtr.Zero;
                P4Bridge.SetTaggedOutputCallbackFn(pServer, IntPtr.Zero);

                ErrorCallbackFn_Int = null;
                pErrorCallbackFn = IntPtr.Zero;
                P4Bridge.SetErrorCallbackFn(pServer, IntPtr.Zero);

                InfoResultsCallbackFn_Int = null;
                pInfoResultsCallbackFn = IntPtr.Zero;
                P4Bridge.SetInfoResultsCallbackFn(pServer, IntPtr.Zero);

                TextResultsCallbackFn_Int = null;
                pTextResultsCallbackFn = IntPtr.Zero;
                P4Bridge.SetTextResultsCallbackFn(pServer, IntPtr.Zero);

                BinaryResultsCallbackFn_Int = null;
                pBinaryResultsCallbackFn = IntPtr.Zero;
                P4Bridge.SetBinaryResultsCallbackFn(pServer, IntPtr.Zero);

                PromptCallbackFn_Int = null;
                pPromptCallbackFn = IntPtr.Zero;
                P4Bridge.SetPromptCallbackFn(pServer, IntPtr.Zero);

                _disposed = true;
            }
            catch (Exception ex)
            {
                LogFile.LogException("P4Server.Dispose Error", ex);
            }

        }
        #region IDisposable Members

        /// <summary>
        /// For IDispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        /// <summary>
        /// Reconnect to the server in the event the connection is lost
        /// </summary>
        public void Reconnect()
        {
            if (pServer != IntPtr.Zero)
            {
                P4Bridge.ReleaseConnection(pServer);
                pServer = IntPtr.Zero;
            }
            CurrentEncodeing = P4Encoding.ASCII;

            // encode the username, password, and workspace name in UTF-8, we
            // won't know if the client supports Unicode until after connect
            // returns
            using (PinnedByteArray pUser = MarshalStringToIntPtr(_user),
                                   pPass = MarshalStringToIntPtr(_pass),
                                   pClient = MarshalStringToIntPtr(_ws_client))
            {
                if (logfn != null)
                    pLogFn = Marshal.GetFunctionPointerForDelegate(logfn);

                pServer = P4Bridge.ConnectW(_server, pUser, pPass, pClient, pLogFn);

            }

            if (pServer == IntPtr.Zero)
            {

                ConnectionError = ConnectionErrorInt;
                P4Exception.Throw(ConnectionError);
                return;
            }
            else
            {
                ConnectionError = null;
            }

            isUnicode = P4Bridge.IsUnicode(pServer);
            if (isUnicode)
            {
                CurrentEncodeing = P4Encoding.utf8;

                // SetCharacterSet(Charset[(int)CurrentEncodeing], Charset[(int)P4Encoding.utf16bom]);
                SetCharacterSet("utf8", "none");
            }

            requiresLogin = P4Bridge.UseLogin(pServer);

            // Link the callbacks from the bridge dll to their corresponding events
            SetInfoResultsCallback();
            SetTaggedOutputCallback();
            SetErrorCallback();
            SetTextResultsCallback();
            SetBinaryResultsCallback();
            SetPromptCallback();
            SetResolveCallback();
            SetResolveACallback();
            SetParallelTransferCallback();

            if (_cwd != null)
            {
                CurrentWorkingDirectory = _cwd;
            }
            ProgramName = _prog_name;
            ProgramVersion = _prog_ver;

            // *** Don't login using the login command. If we are reconnecting
            // *** after a timeout, doe not want to risk the login timing out and
            // *** throwing us in an infinite loop
            // We've theoretically already logged in, so if using tickets, _pass
            // should hold the ticket.
            //Login(_pass, null);
        }

        /// <summary>
        /// Close the connection to a P4 Server
        /// </summary>
        /// <remarks>
        /// Called by the Dispose() method
        /// </remarks>
        public void Close()
        {
            if (pServer != IntPtr.Zero)
            {
                lock (runLock)
                {
                    try
                    {
                        if (DisconnectTimer != null)
                        {
                            DisconnectTimer.Stop();
                            DisconnectTimer.Dispose();
                        }

                        if (lastCmdId != 0)
                        {
                            Debug.Trace(
                                string.Format("In Close(), currently running command {0}", lastCmdId));
                            // can't cancel commands from the original list, as that might change the contents
                            // of the list and crash the enumerator

                            P4Bridge.CancelCommand(pServer, lastCmdId);
                            Thread.Sleep(TimeSpan.FromSeconds(1)); // wait for commands to exit
                        }
                    }
                    finally
                    {
                        if (P4Bridge.ReleaseConnection(pServer) == 0)
                        {
                            Debug.Trace("Could not close connection, commands are still running");
                        }
                        pServer = IntPtr.Zero;
                    }
                }
            }
        }

        /// <summary>
        /// Need to use Unicode when marshalling to/from the P4 server
        /// </summary>
        public bool UseUnicode
        {
            get
            {
                return isUnicode;
            }
        }

        /// <summary>
        /// What API level does the server support
        /// </summary>
        public int ApiLevel
        {
            get
            {
                return apiLevel;
            }
        }
        /// <summary>
        /// The server requires a client to use the login command to pass credentials.
        /// </summary>
        public bool RequiresLogin
        {
            get
            {
                return requiresLogin;
            }
        }
        System.Timers.Timer DisconnectTimer = null;

        private void OnDisconnectTimer(object source, ElapsedEventArgs e)
        {
            lock (runLock)
            {
                LogFile.LogMessage(4, "P4Server", "Handling disconnect timer");
                if (pServer == IntPtr.Zero)
                {
                    return;
                }

                // have we actually timed out, or did another command run after we acquired the lock?
                if ((DateTime.Now - lastRunCommand).TotalMilliseconds > IdleDisconnectWaitTime)
                {
                    LogFile.LogMessage(4, "P4Server", "Disconnect");
                    DisconnectTimer.Stop();
                    Disconnect();
                    LogFile.LogMessage(4, "P4Server", "Disconnect complete");
                }
                else
                {
                    LogFile.LogMessage(4, "P4Server", String.Format("skipping disconnect, a new command was run at {0}", lastRunCommand));
                }
            }
        }

        public void Disconnect()
        {
            if (pServer != IntPtr.Zero)
            {
                P4Bridge.Disconnect(pServer);
            }
        }

        public bool UrlHandled()
        {
            return P4Bridge.UrlLaunched();
        }

        public bool IsConnected()
        {
            if (pServer == IntPtr.Zero)
                return false;
            return P4Bridge.IsConnected(pServer) != 0;
        }

        private class RunCommandThreadParam
        {
            public string cmd;
            public bool tagged;
            public String[] args;
            public int argc;
            public bool Results;
            public uint cmdId;

            public Exception RunCmdException = null;
        }
        /// <summary>
        /// Time for a command to run before allowing the client/user to cancel;
        /// </summary>
        public TimeSpan KeepAliveDelay = TimeSpan.FromSeconds(5);

        [Obsolete("Use RunCmdTimeout")]
        public TimeSpan RunCmdTimout
        {
            get { return RunCmdTimeout; }
            set { RunCmdTimeout = value; }
        }

#if DEBUG
        // long delays for debugging so it won't time out / disconnect while stepping through code

        public TimeSpan RunCmdTimeout = TimeSpan.FromSeconds(5000);

        public double IdleDisconnectWaitTime = 5000;
#else
		public TimeSpan RunCmdTimeout = TimeSpan.FromSeconds(30);

		public double IdleDisconnectWaitTime = 5000;
#endif

        private Dictionary<uint, CmdContactTimer> RunCmdLastContactMap = null;

        public bool IsCommandPaused(uint cmdId)
        {
            return RunCmdLastContactMap[cmdId].IsPaused;

        }
        public void PauseRunCmdTimer(uint cmdId)
        {
            if (RunCmdLastContactMap == null || !RunCmdLastContactMap.ContainsKey(cmdId))
            {
                return;
            }
            RunCmdLastContactMap[cmdId].Pause();
        }

        public void ContinueRunCmdTimer(uint cmdId)
        {
            if (RunCmdLastContactMap == null || !RunCmdLastContactMap.ContainsKey(cmdId))
            {
                return;
            }
            RunCmdLastContactMap[cmdId].Continue();
        }

        public class CmdContactTimer : IDisposable
        {
            private uint cmdId = 0;
            private int CallbackCnt = 0;
            private Dictionary<uint, CmdContactTimer> RunCmdLastContactMap = null;
            DateTime LastContact = DateTime.MaxValue;

            private CmdContactTimer() { }
            public CmdContactTimer(Dictionary<uint, CmdContactTimer> runCmdLastContactMap, uint _cmdId)
            {
                if (runCmdLastContactMap == null)
                {
                    throw new ArgumentNullException("runCmdLastContactMap");
                }

                lock (runCmdLastContactMap)
                {
                    cmdId = _cmdId;
                    CallbackCnt = 0;
                    LastContact = DateTime.Now;

                    RunCmdLastContactMap = runCmdLastContactMap;

                    RunCmdLastContactMap[cmdId] = this;
                }
            }

            public void Pause()
            {
                lock (RunCmdLastContactMap)
                {
                    if (RunCmdLastContactMap == null || !RunCmdLastContactMap.ContainsKey(cmdId))
                    {
                        return;
                    }
                    LastContact = DateTime.MaxValue;
                    CallbackCnt++;
                }
            }

            public void Continue()
            {
                lock (RunCmdLastContactMap)
                {
                    if (RunCmdLastContactMap == null || !RunCmdLastContactMap.ContainsKey(cmdId))
                    {
                        return;
                    }
                    CallbackCnt--;

                    if (CallbackCnt == 0)
                    {
                        LastContact = DateTime.Now;
                    }
                }
            }

            #region IDisposable Members

            public void Dispose()
            {
                lock (RunCmdLastContactMap)
                {
                    if (RunCmdLastContactMap != null || RunCmdLastContactMap.ContainsKey(cmdId))
                    {
                        RunCmdLastContactMap.Remove(cmdId);
                    }
                }
            }

            public bool IsPaused
            {
                get
                {
                    lock (RunCmdLastContactMap)
                    {
                        return LastContact == DateTime.MaxValue;
                    }
                }
            }

            public bool OverDue(TimeSpan RunCmdTimeout)
            {
                lock (RunCmdLastContactMap)
                {
                    return ((DateTime.Now - RunCmdLastContactMap[cmdId].LastContact) > RunCmdTimeout);
                }

            }
            #endregion
        }

        //need a unique id to send to the Client for the IKeepAlive interface
        private int next_cmdId = 1;
        private object cmdIdSyncObj = new object();
        public uint getCmdId()
        {
            lock (cmdIdSyncObj)
            {
                if (next_cmdId >= ushort.MaxValue)
                {
                    next_cmdId = 1;
                }
                // even though this is unsigned, don't want a negative integer, so mask off the high bit
                uint v = (((uint)Thread.CurrentThread.ManagedThreadId) << 16) & 0x7FFF0000;
                v |= ((uint)Interlocked.Increment(ref next_cmdId)) & 0x0000FFFF;
                return v;
            }
        }

        // lastRunCommand track the last start/finish time of a command
        private DateTime lastRunCommand = new DateTime();
        // record the last cmdId for Close()'s cancel operation
        private uint lastCmdId = 0;
        // runLock synchronizes the idle disconnect timeout and the main execution thread
        private Object runLock = new Object();

        //check for generic parallel error
        private bool IsGenericParallelError(P4ClientError err)
        {
            // Adjust this check to match your actual generic error code/message.
            // The default message from P4BridgeServer.cpp is "Error detected during parallel operation"
            return err != null && err.ErrorMessage != null &&
                   err.ErrorMessage.Contains("Error detected during parallel operation");
        }

        /// <summary>
        /// Run a P4 command on the P4 Server
        /// </summary>
        /// <remarks>
        /// If the command fails, the error output will contain one or more
        /// errors generated by the P4 server.
        /// </remarks>
        /// <param name="cmd">Command code</param>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        /// <param name="tagged">Use tagged output for the results</param>
        /// <param name="args">Arguments for the command</param>
        /// <param name="argc">Argument count</param>
        /// <returns>Success/Failure</returns>
        public bool RunCommand(string cmd,
                                uint cmdId,
                                bool tagged,
                                String[] args,
                                int argc)
        {
            // if there is an owner thread id present, make sure it matches ours
            if (ownerThread >= 0 && Thread.CurrentThread.ManagedThreadId != ownerThread)
            {
                // TODO: throw an error?
                throw new P4.P4Exception(ErrorSeverity.E_FATAL, String.Format("Mismatched thread id: {0} should be {1}", Thread.CurrentThread.ManagedThreadId, ownerThread));
            }

            if (_disposed)
            {
                throw new P4.P4Exception(ErrorSeverity.E_FATAL, "trying to run a command on a disposed server");
            }

            bool results = false;

            RunCommandThreadParam CmdParams = new RunCommandThreadParam();
            CmdParams.Results = false;
            CmdParams.cmd = cmd;
            CmdParams.tagged = tagged;
            CmdParams.args = args;
            CmdParams.argc = argc;
            CmdParams.cmdId = cmdId;

            lock (runLock)
            {
                lastRunCommand = DateTime.Now;
                lastCmdId = cmdId;
                try
                {
                    // reset the parallel errors and servers lists
                    parallelErrors = null;
                    parallelServers = null;

                    // set the transfer function now that we have locked the runner
                    if (ParallelTransferCallbackFn != IntPtr.Zero)
                        P4Bridge.SetParallelTransferCallbackFn(pServer, ParallelTransferCallbackFn);

                    if (DisconnectTimer != null && DisconnectTimer.Enabled)
                        DisconnectTimer.Stop();

                    if (!isUnicode)
                    {
                        results = P4Bridge.RunCommandA(pServer,
                                                        cmd,
                                                        cmdId,
                                                        tagged,
                                                        args,
                                                        argc);
                    }
                    else
                    {
                        using (PinnedByteArrays args_b = MarshalStringArrayToIntPtrArray(args,
                                                                                argc))
                        {
                            results = P4Bridge.RunCommandW(pServer,
                                                            cmd,
                                                            cmdId,
                                                            tagged,
                                                            (IntPtr[])args_b,
                                                            argc);
                        }
                    }

                    if (!results)
                    {
                        Debug.Trace(string.Format("RunCommand Command [{0}] failed!", cmdId));
                        // error
                        IntPtr pObj = P4Bridge.GetErrorResults(pServer, cmdId);

                        if (pObj == IntPtr.Zero)
                        {
                            // no errors from command, so check for a connection error
                            ConnectionError = ConnectionErrorInt;
                            if (ConnectionError == null)
                            {
                                Debug.Trace(string.Format("RunCommand Command [{0}] failed! ConnectionError=null", cmdId));
                                P4Exception.Throw(cmd, args, ErrorSeverity.E_FATAL, "Unknown Problem, can't continue");
                            }
                            else
                            {
                                Debug.Trace(string.Format("RunCommand Command [{1}] failed! ConnectionError={0}", ConnectionError, cmdId));
                                P4Exception.Throw(cmd, args, ConnectionError);
                            }
                        }
                        else
                        {
                            P4ClientErrorList _errorList = new P4ClientErrorList(this, pObj);
                            // when we cancel a command, throw the cancelled connection exception
                            // the client error is  MsgRpc::Break - "TCP receive interrupted by client"
                            foreach (P4ClientError e in _errorList)
                            {
                                if (e.ErrorCode == P4ClientError.MsgRpc_Break)
                                    throw new P4CommandCanceledException(String.Format("Command {0} cancelled", cmdId));
                            }
                            // If there are multiple errors, and one is the generic parallel error, remove it
                            if (_errorList != null && _errorList.Count > 1)
                            {
                                _errorList.RemoveAll(IsGenericParallelError);
                            }
                            // If there is only one error and it's the generic parallel error, but parallelErrors is set, replace it
                            if (_errorList != null && _errorList.Count == 1 && IsGenericParallelError(_errorList[0]) && parallelErrors != null && parallelErrors.Count > 0)
                            {
                                _errorList.Clear();
                                _errorList.AddRange(parallelErrors);
                            }
                            P4Exception.Throw(cmd, args, _errorList, GetInfoResults(cmdId));
                        }
                        return false;
                    }
                    else
                    {
                        // may be some warnings in the list, so fetch it if it is not null
                        IntPtr pObj = P4Bridge.GetErrorResults(pServer, cmdId);
                        P4ClientErrorList _errorList = null;
                        if (pObj != IntPtr.Zero)
                        {
                            _errorList = new P4ClientErrorList(this, pObj);
                        }
                        ConnectionError = null;
                    }
                }
                finally
                {
                    // update the lastRunCommand
                    lastRunCommand = DateTime.Now;
                    lastCmdId = 0;
                    if (IdleDisconnectWaitTime > 0)
                    {
                        if (DisconnectTimer == null)
                        {
                            DisconnectTimer = new System.Timers.Timer(IdleDisconnectWaitTime);
                            DisconnectTimer.AutoReset = true;
                            DisconnectTimer.Elapsed += new ElapsedEventHandler(OnDisconnectTimer);
                            DisconnectTimer.Start();
                        }
                        else if (DisconnectTimer.Enabled == false)
                        {
                            DisconnectTimer.Start();
                        }
                    }
                }
            }
            return results;
        }

        /// <summary>
        /// Mostly for testing, provide a way to tell if a parallel operation has started
        /// </summary>
        public int GetParallelOperationCount()
        {
            return (parallelServers == null) ? 0 : parallelServers.Count;
        }

        /// <summary>
        /// Cancel a running command
        /// </summary>
        /// <param name="CmdId">Unique Id for the run of the command</param>
        public void CancelCommand(uint CmdId)
        {

            // special case for parallel operations
            if (parallelServers != null && parallelServers.Count > 0)
            {
                foreach (P4Server s in parallelServers)
                {
                    s.CancelCommand(CmdId);
                }
            }

            P4Bridge.CancelCommand(pServer, CmdId);
        }

        /// <summary>
        /// Delegate used to send tagged output as it is generated.
        /// </summary>
        /// <remarks>
        /// This delegate will send a complete object after all of its fields
        /// have been received by the callback from the bridge dll.
        /// </remarks>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        /// <param name="ObjId"></param>
        /// <param name="Obj"></param>
        public delegate void TaggedOutputDelegate(uint cmdId,
                                                    int ObjId,
                                                    TaggedObject Obj);

        /// <summary>
        /// Event to broadcast tagged output
        /// </summary>
        public event TaggedOutputDelegate TaggedOutputReceived;

        /// <summary>
        /// Get the tagged output generated by a command
        /// </summary>
        /// <returns>A list of TaggedObjects comprising the tagged output.
        /// </returns>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        public TaggedObjectList GetTaggedOutput(uint cmdId)
        {
            int nData = P4Bridge.GetTaggedOutputCount(pServer, cmdId);
            IntPtr pData = P4Bridge.GetTaggedOutput(pServer, cmdId);

            if (pData == IntPtr.Zero)
            {
                return null;
            }

            // use a StrDictListIterator to return all of the objects and
            //  their keys.
            StrDictListIterator data = new StrDictListIterator(this, pData);
            TaggedObjectList objects = new TaggedObjectList(nData);

            while (data.NextItem())
            {
                TaggedObject currentObject = new TaggedObject();
                objects.Add(currentObject);

                KeyValuePair kv = null;
                while ((kv = data.NextEntry()) != null)
                {
                    if (string.IsNullOrEmpty(kv.Key) == false)
                    {
                        currentObject[kv.Key] = kv.Value;
                    }
                }
            }

            // clean up the pData object pointer in the bridge
            P4Bridge.Release(pData);

            return (TaggedObjectList)objects;
        }

        /// <summary>
        /// Delegate used to send errors as they are generated.
        /// </summary>
        /// <remarks>
        /// This delegate will send a block of data for each call received by
        /// the callback from the bridge dll.
        /// </remarks>
        /// <param name="cmdId">Command Id of the command causing the error</param>
        /// <param name="severity">Severity of the error</param>
        /// <param name="errorNumber">Error number for the error</param>
        /// <param name="data">Error message</param>
        public delegate void ErrorDelegate(uint cmdId, int severity, int errorNumber, String data);
        /// <summary>
        /// Holds the call back passed to the bridge used to receive the
        /// raw  data
        /// </summary>
        P4CallBacks.ErrorDelegate
            ErrorCallbackFn_Int = null;

        /// <summary>
        /// Broadcast errors received
        /// </summary>
        public event ErrorDelegate ErrorReceived;

        /// <summary>
        /// Get a list of errors (if any) generated by a command
        /// </summary>
        /// <returns>A list of P4ClientErrors, null if no errors</returns>
        public P4ClientErrorList GetErrorResults(uint cmdId)
        {
            // special case for parallel operations, return the parallel list if non-empty
            if ((parallelErrors?.Count ?? 0) > 0)
                return parallelErrors;

            IntPtr pErr = P4Bridge.GetErrorResults(pServer, cmdId);

            if (pErr != IntPtr.Zero)
            {
                return new P4ClientErrorList(this, pErr);
            }
            return null;
        }

        /// <summary>
        /// Delegate used to send Info Results as they are generated.
        /// </summary>
        /// <remarks>
        /// This delegate will send a block of data for each call received by
        /// the callback from the bridge dll.
        /// </remarks>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        /// <param name="msgId"></param>
        /// <param name="level">Server supplied message level</param>
        /// <param name="data">Server supplied message data</param>
        public delegate void InfoResultsDelegate(uint cmdId, int msgId, int level, String data);

        /// <summary>
        /// Broadcast event for info results
        /// </summary>
        public event InfoResultsDelegate InfoResultsReceived;

        /// <summary>
        /// Get the information messages generated by the previous command
        /// </summary>
        /// <remarks>
        /// Each message is formatted as follows
        /// l:Message text
        ///     where l is a single digit representing the message level
        /// </remarks>
        /// <returns>List of messages</returns>
        public P4ClientInfoMessageList GetInfoResults(uint cmdId)
        {
            int nInfoOut = P4Bridge.GetInfoResultsCount(pServer, cmdId);
            IntPtr pInfoOut = P4Bridge.GetInfoResults(pServer, cmdId);

            if (pInfoOut != IntPtr.Zero)
            {
                return new P4ClientInfoMessageList(this, pInfoOut, nInfoOut);
            }
            return null;
        }

        /// <summary>
        /// Delegate used to send Text Results as they are generated.
        /// </summary>
        /// <remarks>
        /// This delegate will send a block of data for each call received by
        /// the callback from the bridge dll.
        /// </remarks>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        /// <param name="data">Text results generated by the command</param>
        public delegate void TextResultsDelegate(uint cmdId, String data);

        /// <summary>
        /// Broadcast event for text results
        /// </summary>
        public event TextResultsDelegate TextResultsReceived;

        /// <summary>
        /// Get the complete text results for the last command
        /// </summary>
        /// <returns></returns>
        /// <param name="cmdID">Unique Id for the run of the command</param>
        public String GetTextResults(uint cmdID)
        {
            IntPtr pTextOut = P4Bridge.GetTextResults(pServer, cmdID);

            return MarshalPtrToString(pTextOut);
        }

        /// <summary>
        /// Delegate used to send binary output as it is generated.
        /// </summary>
        /// <remarks>
        /// This delegate will send a block of data for each call received by
        /// the callback from the bridge dll.
        /// </remarks>
        /// <param name="data">Binary data generated by a command</param>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        public delegate void BinaryResultsDelegate(uint cmdId, byte[] data);

        /// <summary>
        /// Broadcast event for binary data
        /// </summary>
        public event BinaryResultsDelegate BinaryResultsReceived;

        /// <summary>
        /// Get the complete binary results for the last command
        /// </summary>
        /// <returns>The binary data</returns>
        public byte[] GetBinaryResults(uint cmdId)
        {
            int byteCount = P4Bridge.GetBinaryResultsCount(pServer, cmdId);
            if (byteCount <= 0)
                return null;

            IntPtr pData = P4Bridge.GetBinaryResults(pServer, cmdId);
            if (pData == IntPtr.Zero)
                return null;

            return MarshalPtrToByteArrary(pData, byteCount);
        }


        /// <summary>
        /// Delegate used to commands as they are executed.
        /// </summary>
        /// <param name="data">Command line executed by the command</param>
        public delegate void CommandEchoDelegate(String data);

        /// <summary>
        /// Broadcast event for text results
        /// </summary>
        public event CommandEchoDelegate CommandEcho;

        /// <summary>
        /// Delegate used to commands along with response time as they are executed.
        /// </summary>
        /// <param name="data">Command line along with response time executed by the command</param>
        public delegate void ResponseTimeEchoDelegate(String data);

        /// <summary>
        /// Broadcast event for text results
        /// </summary>
        public event ResponseTimeEchoDelegate ResponseTimeEcho;

        /// <summary>
        /// Broadcast a the command line (cmd and args) on the CommandEcho event
        /// </summary>
        /// <remarks>
        /// Used to echo an executed command line back to the client
        /// </remarks>
        /// <param name="cmd">The P4 command.</param>
        /// <param name="args">The flags and parameters for the command.</param>
        public void EchoCommand(string cmd, StringList args)
        {
            if (CommandEcho != null)
            {
                string commandLine = BuildCmdLineString(cmd, args);
                CommandEcho(commandLine);
            }
        }

        /// <summary>
        /// Broadcast a the command line (cmd, args and response time) on the ResponseTimeEcho event
        /// </summary>
        /// <remarks>
        /// Used to echo an executed command line with response time back to the client
        /// </remarks>
        /// <param name="cmd">The P4 command.</param>
        /// <param name="args">The flags and parameters for the command.</param>
        /// <param name="responseTime">Response time taken to fetch the results.</param>
        public void EchoResponseTime(string cmd, StringList args, TimeSpan responseTime)
        {
            if (ResponseTimeEcho != null)
            {
                string commandLine = BuildCmdLineString(cmd, args);
                ResponseTimeEcho(string.Format("Time taken to fetch result of '{0}': {1} ", commandLine, responseTime));
            }
        }

        /// <summary>
        /// Broadcast a string on the CommandEcho event
        /// </summary>
        /// <remarks>
        /// Used to echo command data back to the client
        /// </remarks>
        /// <param name="str">The string.</param>
        public void EchoCommand(string str)
        {
            if (CommandEcho != null)
            {
                CommandEcho(str);
            }
        }

        /// <summary>
        /// The data set for use by a command
        /// </summary>
        /// <remarks>
        /// If a command requires data not passed on the command line, such as
        /// a client spec, it is passed to the P$ server by setting the data
        /// set in the P4 api.
        /// </remarks>
        public void SetDataSet(uint cmdId, string value)
        {
            if (isUnicode)
            {
                using (PinnedByteArray pData = MarshalStringToIntPtr(value))
                {
                    P4Bridge.SetDataSetW(pServer, cmdId, pData);
                }
            }
            else
            {
                P4Bridge.SetDataSetA(pServer, cmdId, value);
            }
        }
        public String GetDataSet(uint cmdId)
        {
            IntPtr pData = P4Bridge.GetDataSet(pServer, cmdId);

            return MarshalPtrToString(pData);
        }

        /// <summary>
        /// Delegate used to provide a custom handler for input prompts from the p4api.
        /// </summary>
        /// <param name="cmdId">Unique Id for the run of the command</param>
        /// <param name="msg"></param>
        /// <param name="displayText"></param>
        /// <returns></returns>
        public delegate String PromptHandlerDelegate(uint cmdId, String msg, bool displayText);

        /// <summary>
        /// Delegate used to process prompts for input from the server.
        /// </summary>
        public PromptHandlerDelegate PromptHandler;

        /// <summary>
        /// Delegate definition for the parallel operations callback.
        /// </summary>
        /// <param name="pServer">Pointer the the bridge server (needed?)</param>
        /// <param name="cmd">Prompt message from the server</param>
        /// <param name="args">array of arguments</param>
        /// <param name="argCount">number of arguments</param>
        /// <param name="dictIter">dictionary of variables</param>
        /// <param name="threads">number of threads to launch</param>
        public delegate int ParallelTransferDelegate(IntPtr pServer, String cmd, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] String[] args, uint argCount, IntPtr dictIter, uint threads);

        /// <summary>
        /// Delegate used to provide a custom handler for Resolve callbacks passing a ClientMerge object from the p4api.
        /// </summary>
        /// <returns></returns>
        public delegate P4ClientMerge.MergeStatus ResolveHandlerDelegate(uint cmdId, P4ClientMerge Merger);

        /// <summary>
        /// Delegate used to provide a custom handler for Resolve callbacks passing a ClientMerge object from the p4api.
        /// </summary>
        public ResolveHandlerDelegate ResolveHandler;

        /// <summary>
        /// Delegate used to provide a custom handler for Resolve callbacks passing a ClientResolve object from the p4api.
        /// </summary>
        /// <returns></returns>
        public delegate P4ClientMerge.MergeStatus ResolveAHandlerDelegate(uint cmdId, P4ClientResolve Resolver);

        /// <summary>
        /// Delegate used to provide a custom handler for Resolve callbacks passing a ClientResolve object from the p4api.
        /// </summary>
        public ResolveAHandlerDelegate ResolveAHandler;

        /// <summary>
        /// The parameters used by the connection
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public void SetConnectionData(string port, string user, string password, string client)
        {
            _ws_client = client;
            if (isUnicode)
            {
                using (PinnedByteArray pPort = MarshalStringToIntPtr(port),
                        pUser = MarshalStringToIntPtr(user),
                        pPassword = MarshalStringToIntPtr(password),
                        pClient = MarshalStringToIntPtr(client))
                {
                    P4Bridge.set_connectionW(pServer, pPort, pUser, pPassword, pClient);
                }
            }
            else
            {
                P4Bridge.set_connectionA(pServer, port, user, password, client);
            }
        }

        /// <summary>
        /// The client workspace used by the connection
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public String Client
        {
            get
            {
                IntPtr pval = P4Bridge.get_client(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _ws_client = value;
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(value))
                    {
                        P4Bridge.set_clientW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_clientA(pServer, value);
                }
            }
        }

        /// <summary>
        /// The user name used by the connection
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public String User
        {
            get
            {
                IntPtr pval = P4Bridge.get_user(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _user = value;
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(value))
                    {
                        P4Bridge.set_userW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_userA(pServer, value);
                }
            }
        }

        /// <summary>
        /// The hostname:port used by the connection
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public String Port
        {
            get
            {
                IntPtr pval = P4Bridge.get_port(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _server = value;
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(value))
                    {
                        P4Bridge.set_portW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_portA(pServer, value);
                }
            }
        }

        /// <summary>
        /// The user's password used by the connection
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public String Password
        {
            get
            {
                IntPtr pval = P4Bridge.get_password(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _pass = value ?? "";  // cleaner to pass empty strings instead of nulls
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(_pass))
                    {
                        P4Bridge.set_passwordW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_passwordA(pServer, _pass);
                }
            }
        }

        /// <summary>
        /// The program name used by the connection
        /// </summary>
        /// <remarks>
        /// The program name and version are recorded in the server logs when
        /// accessed by the client
        /// </remarks>
        public String ProgramName
        {
            get
            {
                IntPtr pval = P4Bridge.get_programName(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _prog_name = value;
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(_prog_name))
                    {
                        P4Bridge.set_programNameW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_programNameA(pServer, _prog_name);
                }
            }
        }

        /// <summary>
        /// The program version used by the connection
        /// </summary>
        /// <remarks>
        /// The program name and version are recorded in the server logs when
        /// accessed by the client
        /// </remarks>
        public String ProgramVersion
        {
            get
            {
                IntPtr pval = P4Bridge.get_programVer(pServer);

                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                _prog_ver = value;
                if (isUnicode)
                {
                    using (PinnedByteArray pData = MarshalStringToIntPtr(_prog_ver))
                    {
                        P4Bridge.set_programVerW(pServer, pData);
                    }
                }
                else
                {
                    P4Bridge.set_programVerA(pServer, _prog_ver);
                }
            }
        }

        /// <summary>
        /// The current working directory (cwd) used by the p4 server
        /// </summary>
        /// <remarks>
        /// The  properties, client, port, user, and password,
        /// represent the criteria used to connect to a P4 server. If one or
        /// more is changed, the bridge will drop the current connection if any
        /// and attempt to connect to the (possibly different) P4 server when
        /// the next command is executed. If it is desirable to validate the
        /// connection, execute a command.
        /// </remarks>
        public String CurrentWorkingDirectory
        {
            get
            {
                IntPtr pval = P4Bridge.get_cwd(pServer);
                String ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
            set
            {
                try
                {
                    _cwd = value;
                    if (isUnicode)
                    {
                        using (PinnedByteArray pData = MarshalStringToIntPtr(_cwd))
                        {
                            P4Bridge.set_cwdW(pServer, pData);
                        }
                    }
                    else
                    {
                        P4Bridge.set_cwdA(pServer, _cwd);
                    }
                }
                catch (Exception ex)
                {
                    LogFile.LogException("P4Bridge Error", ex);
                }
            }
        }


        /// <summary>
        /// The character set used by the connection
        /// </summary>
        /// <remarks>
        /// The character set used to connect to Unicode servers is set by the
        /// bridge dll automatically (possibly overridden by P4CHARSET) based
        /// on the current Windows code page.
        /// </remarks>
        public String CharacterSet
        {
            get
            {
                IntPtr pval = P4Bridge.get_charset(pServer);

                string ret = Marshal.PtrToStringAnsi(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }

            set
            {
                P4Bridge.set_charset(pServer, value, "none");  // Set Contents Charset,  Filenames stay at utf8
            }
        }

        /// <summary>
        /// The config file used by the connection
        /// </summary>
        public String Config
        {
            get
            {
                IntPtr pval = P4Bridge.get_config(pServer);

                string val = MarshalPtrToStringUtf8_Int(pval);
                P4Bridge.ReleaseString(pval);

                return val;
            }
        }

        static public void ConnectionInfoFromPath(String dir, out String port, out String user, out String client)
        {
            IntPtr tmpConnection = P4Bridge.ConnectionFromPath(dir);
            user = MarshalAndRelease(P4Bridge.get_user(tmpConnection));
            port = MarshalAndRelease(P4Bridge.get_port(tmpConnection));
            client = MarshalAndRelease(P4Bridge.get_client(tmpConnection));
            P4Bridge.ReleaseConnection(tmpConnection);
        }

        /// <summary>
        /// The config file that will be used by a given directory
        /// </summary>
        static public String GetConfig(string cwd)
        {
            IntPtr pval = IntPtr.Zero;
            using (PinnedByteArray pData = MarshalStringToIntPtr(Encoding.UTF8, cwd))
            {
                pval = P4Bridge.get_configW(pData);
            }
            if (pval != IntPtr.Zero)
            {
                string val = MarshalPtrToStringUtf8_Int(pval);
                P4Bridge.ReleaseString(pval);
                return val;
            }
            return null;
        }

        /// <summary>
        /// Get an environment setting used by the server, such as user, client, ..
        /// </summary>
        /// <param name="var">The name of the environment variable</param>
        /// <returns></returns>
        static public String Get(string var)
        {
            IntPtr pval = IntPtr.Zero;
            using (PinnedByteArray pData = MarshalStringToIntPtr(Encoding.Default, var))
            {
                pval = P4Bridge.GetW(pData);
            }
            if (pval != IntPtr.Zero)
            {
                string val = MarshalAndRelease(pval);
                return val;
            }
            return null;
        }

        /// <summary>
        /// Set an environment setting used by the server, such as user, client, ..
        /// </summary>
        /// <param name="var">The name of the environment variable</param>
        /// <param name="val">The new value for the environment variable</param>
        /// <returns></returns>
        public static void Set(string var, string val)
        {
            using (PinnedByteArray pData1 = MarshalStringToIntPtr(Encoding.Default, var),
                                    pData2 = MarshalStringToIntPtr(Encoding.Default, val))
            {
                P4Bridge.SetW(pData1, pData2);
            }
        }

        /// <summary>
        /// Update an environment setting used by the server, such as user, client, ..
        /// Makes a local only change which overrides registry, environment etc...
        /// And does not get written into the registry or the environment
        /// use "null" value to remove an existing Update setting.
        /// </summary>
        /// <param name="var">The name of the environment variable</param>
        /// <param name="val">The new value for the environment variable</param>
        public static void Update(string var, string val)
        {
            using (PinnedByteArray pData1 = MarshalStringToIntPtr(Encoding.UTF8, var),
                                    pData2 = MarshalStringToIntPtr(Encoding.UTF8, val))
            {
                P4Bridge.UpdateW(pData1, pData2);
            }
        }

        /// <summary>
        /// Use the C++ API to determine if a file will be ignored
        /// </summary>
        /// <param name="path">The local path of the file</param>
        /// <returns>true if the file will be ignored</returns>
        public static bool IsIgnored(string path)
        {
            using (PinnedByteArray pData = MarshalStringToIntPtr(Encoding.UTF8, path))
            {
                return P4Bridge.IsIgnoredW(pData);
            }
        }

        /// <summary>
        /// Set client debug level for things like "rpc=3"
        ///    Output goes to stdout
        /// </summary>
        /// <param name="level">a string, like "rpc=3" </param>
        /// <returns>null</returns>
        public static void SetDebugLevel(string level)
        {
            using (PinnedByteArray pLevel = MarshalStringToIntPtr(Encoding.UTF8, level))
            {
                P4Bridge.SetDebugLevelW( pLevel);
            }
        }

        /// <summary>
        /// Set client debug level for things like "rpc=3"
        ///    Output goes to specified log file
        /// </summary>
        /// <param name="level">a string, like "rpc=3" </param>
        /// <param name="filename">a filename for the log file </param>
        /// <returns>null</returns>
        public static void SetDebugLevel(string level, string filename)
        {
            using (PinnedByteArray pLevel = MarshalStringToIntPtr(Encoding.UTF8, level),
                                pFilename = MarshalStringToIntPtr(Encoding.UTF8, filename))
            {
                P4Bridge.SetDebugLevelFileW(pLevel, pFilename);
            }
        }

        /// <summary>
        /// Use the C++ API to set the path of the ticket file
        /// </summary>
        /// <param name="ticketFile">full path of the ticket file</param>
        /// <returns>null</returns>
        public void SetTicketFile(string ticketFile)
        {
            using (PinnedByteArray pPass = MarshalStringToIntPtr(ticketFile))
            {
                P4Bridge.set_ticketFileW(pServer, pPass);
            }
        }

        /// <summary>
        /// Use the C++ API to determine the path of the ticket file
        /// for the current connection
        /// </summary>
        /// <returns>The path for the ticket file for the current connection</returns>
        public string GetTicketFile()
        {
            IntPtr pval = P4Bridge.get_ticketFileW(pServer);
            string ret = MarshalPtrToString(pval);
            P4Bridge.ReleaseString(pval);
            return ret;
        }

        /// <summary>
        /// Use the C++ API to get the ticket
        /// </summary>
        /// <param name="path">path to the ticket file</param>
        /// <param name="port">server:port for the connection</param>
        /// <param name="user">user name for the connection</param>
        /// <returns>The existing ticket if any</returns>
        public string GetTicket(string path, string port, string user)
        {
            using (PinnedByteArray pPath = MarshalStringToIntPtr(path),
                 pPort = MarshalStringToIntPtr(port),
             pUser = MarshalStringToIntPtr(user))
            {
                IntPtr pval = P4Bridge.get_ticket(pPath, pPort, pUser);
                string ret = MarshalPtrToString(pval);
                P4Bridge.ReleaseString(pval);
                return ret;
            }
        }

        /// <summary>
        /// Use the C++ API to determine the path of the ticket file
        /// set in the environment
        /// </summary>
        /// <returns>The path for the ticket file as set in P4TICKETS</returns>
        static public string GetEnvironmentTicketFile()
        {
            IntPtr pval = P4Bridge.GetTicketFile();
            if (pval != IntPtr.Zero)
            {
                string val = MarshalPtrToStringUtf8_Int(pval);
                P4Bridge.ReleaseString(pval);
                return val;
            }
            return null;
        }

        /// <summary>
        /// Use the C++ API to find an existing ticket if a file will be ignored
        /// </summary>
        /// <param name="port">server:port for the connection</param>
        /// <param name="user">user name for the connection</param>
        /// <returns>The existing ticket if any</returns>
        static public string GetTicket(string port, string user)
        {
            using (PinnedByteArray pData1 = MarshalStringToIntPtr(Encoding.UTF8, port),
                                    pData2 = MarshalStringToIntPtr(Encoding.UTF8, user))
            {
                IntPtr pval = P4Bridge.GetTicket(pData1, pData2);
                if (pval != IntPtr.Zero)
                {
                    string val = MarshalPtrToStringUtf8_Int(pval);
                    P4Bridge.ReleaseString(pval);
                    return val;
                }
            }
            return null;
        }

        /// <summary>
        /// Use the C++ API to find an existing ticket if a file will be ignored
        /// </summary>
        /// <param name="port">server:port for the connection</param>
        /// <param name="user">user name for the connection</param>
        /// <returns>The existing ticket if any</returns>
        static public Credential GetTicketFileCredential(string port, string user)
        {
            using (PinnedByteArray pData1 = MarshalStringToIntPtr(Encoding.UTF8, port),
                                    pData2 = MarshalStringToIntPtr(Encoding.UTF8, user))
            {
                IntPtr pval = P4Bridge.GetTicket(pData1, pData2);
                if (pval != IntPtr.Zero)
                {
                    string val = MarshalPtrToStringUtf8_Int(pval);
                    P4Bridge.ReleaseString(pval);
                    return new Credential(user, val);
                }
            }
            return null;
        }

        private int ownerThread = -1;   // managed thread ids are >= 0
        public void SetThreadOwner(int threadId)
        {
            ownerThread = threadId;
        }

        public void SetProtocol(string key, string val)
        {
            // Note that this only works if pServer is current disconnected or you reconnect after this call
            P4Bridge.SetProtocol(pServer, key, val);
        }

        /// <summary>
        /// Constructs a command line string by concatenating
        /// a base command with an array of arguments.
        /// </summary>
        /// <param name="cmd">The base command to start with.</param>
        /// <param name="args">An array of arguments to append to the command.</param>
        /// <returns></returns>
        private string BuildCmdLineString(string cmd, StringList args)
        {
            StringBuilder cmdLineArguments = new StringBuilder(cmd);

            if (args != null)
            {
                foreach (var arg in args)
                {
                    if (!string.IsNullOrEmpty(arg))
                    {
                        cmdLineArguments.Append(" ").Append(arg);
                    }
                }
            }

            return cmdLineArguments.ToString();
        }
    }
}

