/*
 * LoggerClient.m
 *
 * version 1.8.2 20-APRIL-2017
 *
 * Main implementation of the NSLogger client side code
 * Part of NSLogger (client side)
 * https://github.com/fpillet/NSLogger
 *
 * BSD license follows (http://www.opensource.org/licenses/bsd-license.php)
 * 
 * Copyright (c) 2010-2017 Florent Pillet All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of  source code  must retain  the above  copyright notice,
 * this list of  conditions and the following  disclaimer. 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.  Neither the  name of  Florent
 * Pillet nor the names of its contributors may be used to endorse or promote
 * products  derived  from  this  software  without  specific  prior  written
 * permission.  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 THE  COPYRIGHT
 * HOLDER OR  CONTRIBUTORS 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.
 * 
 */
#import <sys/time.h>
#import <arpa/inet.h>
#import <stdlib.h>
#import <unistd.h>
#import <pthread.h>
#import <dispatch/dispatch.h>
#import <libkern/OSAtomic.h>
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
#if !TARGET_OS_IPHONE
#import <CoreServices/CoreServices.h>
#endif
#import <sys/types.h>
#import <sys/sysctl.h>
#import <sys/utsname.h>
#import <dlfcn.h>
#import <fcntl.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIDevice.h>
#endif

/* -----------------------------------------------------------------
 * Logger option flags & default options
 * -----------------------------------------------------------------
 */
enum {
  kLoggerOption_LogToConsole						= 0x01,
  kLoggerOption_BufferLogsUntilConnection			= 0x02,
  kLoggerOption_BrowseBonjour						= 0x04,
  kLoggerOption_BrowseOnlyLocalDomain				= 0x08,
  kLoggerOption_UseSSL							= 0x10,
  kLoggerOption_CaptureSystemConsole				= 0x20,
  kLoggerOption_BrowsePeerToPeer					= 0x40
};

#define LOGGER_DEFAULT_OPTIONS	(kLoggerOption_BufferLogsUntilConnection |	\
kLoggerOption_BrowseBonjour |				\
kLoggerOption_BrowsePeerToPeer |			\
kLoggerOption_BrowseOnlyLocalDomain |		\
kLoggerOption_UseSSL)
//| kLoggerOption_CaptureSystemConsole)
//kLoggerOption_LogToConsole

// The Logger struct is no longer public, use the new LoggerGet[...] functions instead
typedef struct Logger Logger;

/* -----------------------------------------------------------------
 * LOGGING FUNCTIONS
 * -----------------------------------------------------------------
 */

#ifdef __cplusplus
extern "C" {
#endif
  
  // Prevents the linker from stripping NSLogger functions
  // This is mainly for linked frameworks to be able to use NSLogger dynamically.
  // If you DO WANT this functionality, you need to define NSLOGGER_ALLOW_NOSTRIP
  // somewhere in the header files included before this one.
#ifdef NSLOGGER_ALLOW_NOSTRIP
#define NSLOGGER_NOSTRIP __attribute__((used))
#else
#define NSLOGGER_NOSTRIP
#endif
  
  // Set the default logger which will be the one used when passing NULL for logge
  extern void LoggerSetDefaultLogger(Logger *aLogger) NSLOGGER_NOSTRIP;
  
  // Get the default logger, create one if it does not exist
  extern Logger *LoggerGetDefaultLogger(void) NSLOGGER_NOSTRIP;
  
  // Checks whether the default logger exists, returns it if YES, otherwise do NO create one
  extern Logger *LoggerCheckDefaultLogger(void) NSLOGGER_NOSTRIP;
  
  // Initialize a new logger, set as default logger if this is the first one
  // Options default to:
  // - logging to console = NO
  // - buffer until connection = YES
  // - browse Bonjour = YES
  // - browse only locally on Bonjour = YES
  extern Logger* LoggerInit(void) NSLOGGER_NOSTRIP;
  
  // Set logger options if you don't want the default options (see above)
  extern void LoggerSetOptions(Logger *logger, uint32_t options) NSLOGGER_NOSTRIP;
  extern uint32_t LoggerGetOptions(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Set Bonjour logging names, so you can force the logger to use a specific service type
  // or direct logs to the machine on your network which publishes a specific name
  extern void LoggerSetupBonjour(Logger *logger, CFStringRef bonjourServiceType, CFStringRef bonjourServiceName) NSLOGGER_NOSTRIP;
  extern CFStringRef LoggerGetBonjourServiceType(Logger *logger) NSLOGGER_NOSTRIP;
  extern CFStringRef LoggerGetBonjourServiceName(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Set Bonjour logging name to be the username who compiled the LoggerClient.m file.
  // This is useful when several NSLogger users are on the same network. This can only be
  // used when NSLogger is integrated as source code or via with CocoaPods.
  extern void LoggerSetupBonjourForBuildUser(void) NSLOGGER_NOSTRIP;
  
  // Directly set the viewer host (hostname or IP address) and port we want to connect to. If set, LoggerStart() will
  // try to connect there first before trying Bonjour
  extern void LoggerSetViewerHost(Logger *logger, CFStringRef hostName, UInt32 port) NSLOGGER_NOSTRIP;
  extern CFStringRef LoggerGetViewerHostName(Logger *logger) NSLOGGER_NOSTRIP;
  extern UInt32 LoggerGetViewerPort(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Configure the logger to use a local file for buffering, instead of memory.
  // - If you initially set a buffer file after logging started but while a logger connection
  //   has not been acquired, the contents of the log queue will be written to the buffer file
  //	 the next time a logging function is called, or when LoggerStop() is called.
  // - If you want to change the buffering file after logging started, you should first
  //   call LoggerStop() the call LoggerSetBufferFile(). Note that all logs stored in the previous
  //   buffer file WON'T be transferred to the new file in this case.
  extern void LoggerSetBufferFile(Logger *logger, CFStringRef absolutePath) NSLOGGER_NOSTRIP;
  extern CFStringRef LoggerGetBufferFile(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Activate the logger, try connecting. You can pass NULL to start the default logger,
  // it will return a pointer to it.
  extern Logger* LoggerStart(Logger *logger) NSLOGGER_NOSTRIP;
  
  //extern void LoggerConnectToHost(CFDataRef address, int port);
  
  // Deactivate and free the logger.
  extern void LoggerStop(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Pause the current thread until all messages from the logger have been transmitted
  // this is useful to use before an assert() aborts your program. If waitForConnection is YES,
  // LoggerFlush() will block even if the client is not currently connected to the desktop
  // viewer. You should be using NO most of the time, but in some cases it can be useful.
  extern void LoggerFlush(Logger *logger, BOOL waitForConnection) NSLOGGER_NOSTRIP;
  
  /* Logging functions. Each function exists in four versions:
   *
   * - one without a Logger instance (uses default logger) and without filename/line/function (no F suffix)
   * - one without a Logger instance but with filename/line/function (F suffix)
   * - one with a Logger instance (use a specific Logger) and without filename/line/function (no F suffix)
   * - one with a Logger instance (use a specific Logger) and with filename/line/function (F suffix)
   *
   * The exception being the single LogMessageCompat() function which is designed to be a drop-in replacement for NSLog()
   *
   */
  
  // Log a message, calling format compatible with NSLog
  extern void LogMessageCompat(NSString *format, ...) NSLOGGER_NOSTRIP;
  
  // Log a message without any formatting (just log the given string)
  extern void LogMessageRaw(NSString *message) NSLOGGER_NOSTRIP;
  extern void LogMessageRawF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message) NSLOGGER_NOSTRIP;
  extern void LogMessageRawToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message) NSLOGGER_NOSTRIP;
  
  // Log a message. domain can be nil if default domain.
  extern void LogMessage(NSString *domain, int level, NSString *format, ...) NS_FORMAT_FUNCTION(3,4) NSLOGGER_NOSTRIP;
  extern void LogMessageF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...) NS_FORMAT_FUNCTION(6,7) NSLOGGER_NOSTRIP;
  extern void LogMessageTo(Logger *logger, NSString *domain, int level, NSString *format, ...) NS_FORMAT_FUNCTION(4,5) NSLOGGER_NOSTRIP;
  extern void LogMessageToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...) NS_FORMAT_FUNCTION(7,8) NSLOGGER_NOSTRIP;
  
  // Log a message. domain can be nil if default domain (versions with va_list format args instead of ...)
  extern void LogMessage_va(NSString *domain, int level, NSString *format, va_list args) NS_FORMAT_FUNCTION(3,0) NSLOGGER_NOSTRIP;
  extern void LogMessageF_va(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args) NS_FORMAT_FUNCTION(6,0) NSLOGGER_NOSTRIP;
  extern void LogMessageTo_va(Logger *logger, NSString *domain, int level, NSString *format, va_list args) NS_FORMAT_FUNCTION(4,0) NSLOGGER_NOSTRIP;
  extern void LogMessageToF_va(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args) NS_FORMAT_FUNCTION(7,0) NSLOGGER_NOSTRIP;
  
  // Send binary data to remote logger
  extern void LogData(NSString *domain, int level, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogDataTo(Logger *logger, NSString *domain, int level, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data) NSLOGGER_NOSTRIP;
  
  // Send image data to remote logger
  extern void LogImageData(NSString *domain, int level, int width, int height, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogImageDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogImageDataTo(Logger *logger, NSString *domain, int level, int width, int height, NSData *data) NSLOGGER_NOSTRIP;
  extern void LogImageDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data) NSLOGGER_NOSTRIP;
  
  // Mark the start of a block. This allows the remote logger to group blocks together
  extern void LogStartBlock(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NSLOGGER_NOSTRIP;
  extern void LogStartBlockTo(Logger *logger, NSString *format, ...) NS_FORMAT_FUNCTION(2,3) NSLOGGER_NOSTRIP;
  
  // Mark the end of a block
  extern void LogEndBlock(void) NSLOGGER_NOSTRIP;
  extern void LogEndBlockTo(Logger *logger) NSLOGGER_NOSTRIP;
  
  // Log a marker (text can be null)
  extern void LogMarker(NSString *text) NSLOGGER_NOSTRIP;
  extern void LogMarkerTo(Logger *logger, NSString *text) NSLOGGER_NOSTRIP;
  
#ifdef __cplusplus
};
#endif



/* NSLogger native binary message format:
 * Each message is a dictionary encoded in a compact format. All values are stored
 * in network order (big endian). A message is made of several "parts", which are
 * typed chunks of data, each with a specific purpose (partKey), data type (partType)
 * and data size (partSize).
 *
 *	uint32_t	totalSize		(total size for the whole message excluding this 4-byte count)
 *	uint16_t	partCount		(number of parts below)
 *  [repeat partCount times]:
 *		uint8_t		partKey		the part key
 *		uint8_t		partType	(string, binary, image, int16, int32, int64)
 *		uint32_t	partSize	(only for string, binary and image types, others are implicit)
 *		.. `partSize' data bytes
 *
 * Complete message is usually made of:
 *	- a PART_KEY_MESSAGE_TYPE (mandatory) which contains one of the LOGMSG_TYPE_* values
 *  - a PART_KEY_TIMESTAMP_S (mandatory) which is the timestamp returned by gettimeofday() (seconds from 01.01.1970 00:00)
 *	- a PART_KEY_TIMESTAMP_MS (optional) complement of the timestamp seconds, in milliseconds
 *	- a PART_KEY_TIMESTAMP_US (optional) complement of the timestamp seconds and milliseconds, in microseconds
 *	- a PART_KEY_THREAD_ID (mandatory) the ID of the user thread that produced the log entry
 *	- a PART_KEY_TAG (optional) a tag that helps categorizing and filtering logs from your application, and shows up in viewer logs
 *	- a PART_KEY_LEVEL (optional) a log level that helps filtering logs from your application (see as few or as much detail as you need)
 *	- a PART_KEY_MESSAGE which is the message text, binary data or image
 *  - a PART_KEY_MESSAGE_SEQ which is the message sequence number (message# sent by client)
 *	- a PART_KEY_FILENAME (optional) with the filename from which the log was generated
 *	- a PART_KEY_LINENUMBER (optional) the linenumber in the filename at which the log was generated
 *	- a PART_KEY_FUNCTIONNAME (optional) the function / method / selector from which the log was generated
 *  - if logging an image, PART_KEY_IMAGE_WIDTH and PART_KEY_IMAGE_HEIGHT let the desktop know the image size without having to actually decode it
 */

// Constants for the "part key" field
#define	PART_KEY_MESSAGE_TYPE	0
#define	PART_KEY_TIMESTAMP_S	1			// "seconds" component of timestamp
#define PART_KEY_TIMESTAMP_MS	2			// milliseconds component of timestamp (optional, mutually exclusive with PART_KEY_TIMESTAMP_US)
#define PART_KEY_TIMESTAMP_US	3			// microseconds component of timestamp (optional, mutually exclusive with PART_KEY_TIMESTAMP_MS)
#define PART_KEY_THREAD_ID		4
#define	PART_KEY_TAG			5
#define	PART_KEY_LEVEL			6
#define	PART_KEY_MESSAGE		7
#define PART_KEY_IMAGE_WIDTH	8			// messages containing an image should also contain a part with the image size
#define PART_KEY_IMAGE_HEIGHT	9			// (this is mainly for the desktop viewer to compute the cell size without having to immediately decode the image)
#define PART_KEY_MESSAGE_SEQ	10			// the sequential number of this message which indicates the order in which messages are generated
#define PART_KEY_FILENAME		11			// when logging, message can contain a file name
#define PART_KEY_LINENUMBER		12			// as well as a line number
#define PART_KEY_FUNCTIONNAME	13			// and a function or method name

// Constants for parts in LOGMSG_TYPE_CLIENTINFO
#define PART_KEY_CLIENT_NAME	20
#define PART_KEY_CLIENT_VERSION	21
#define PART_KEY_OS_NAME		22
#define PART_KEY_OS_VERSION		23
#define PART_KEY_CLIENT_MODEL	24			// For iPhone, device model (i.e 'iPhone', 'iPad', etc)
#define PART_KEY_UNIQUEID		25			// for remote device identification, part of LOGMSG_TYPE_CLIENTINFO

// Area starting at which you may define your own constants
#define PART_KEY_USER_DEFINED	100

// Constants for the "partType" field
#define	PART_TYPE_STRING		0			// Strings are stored as UTF-8 data
#define PART_TYPE_BINARY		1			// A block of binary data
#define PART_TYPE_INT16			2
#define PART_TYPE_INT32			3
#define	PART_TYPE_INT64			4
#define PART_TYPE_IMAGE			5			// An image, stored in PNG format

// Data values for the PART_KEY_MESSAGE_TYPE parts
#define LOGMSG_TYPE_LOG			0			// A standard log message
#define	LOGMSG_TYPE_BLOCKSTART	1			// The start of a "block" (a group of log entries)
#define	LOGMSG_TYPE_BLOCKEND	2			// The end of the last started "block"
#define LOGMSG_TYPE_CLIENTINFO	3			// Information about the client app
#define LOGMSG_TYPE_DISCONNECT	4			// Pseudo-message on the desktop side to identify client disconnects
#define LOGMSG_TYPE_MARK		5			// Pseudo-message that defines a "mark" that users can place in the log flow

// Default Bonjour service identifiers
#define LOGGER_SERVICE_TYPE_SSL	CFSTR("_nslogger-ssl._tcp")
#define LOGGER_SERVICE_TYPE		CFSTR("_nslogger._tcp")




/* --------------------------------------------------------------------------------
 * IMPLEMENTATION NOTES:
 *
 * The logger runs in a separate thread. You can start
 * logging very early (as soon as your code starts running), logs will be
 * buffered and sent to the log viewer as soon as a connection is acquired.
 * This makes the logger suitable for use in conditions where you usually
 * don't have a connection to a remote machine yet (early wakeup, network
 * down, etc).
 *
 * When you call one of the public logging functions, the logger is designed
 * to return to your application as fast as possible. It enqueues logs to
 * send for processing by its own thread, while your application keeps running.
 *
 * The logger does buffer logs while not connected to a desktop
 * logger. It uses Bonjour to find a logger on the local network, and can
 * optionally connect to a remote logger identified by an IP address / port
 * or a Host Name / port.
 *
 * The logger can optionally output its logs to the console, like NSLog().
 *
 * The logger can optionally buffer its logs to a file for which you specify the
 * full path. Upon connection to the desktop viewer, the file contents are
 * transmitted to the viewer prior to sending new logs. When the whole file
 * content has been transmitted, it is emptied.
 *
 * Multiple loggers can coexist at the same time. You can perfectly use a
 * logger for your debug traces, and another that connects remotely to help
 * diagnose issues while the application runs on your user's device.
 *
 * The logger can optionally capture stdout and stderr. When running an
 * application from the IDE, this will automatically capture everything that
 * goes into the debugger console log, and insert it in the stream of logs
 * sent to the viewer.
 *
 * Using the logger's flexible packet format, you can customize logging by
 * creating your own log types, and customize the desktop viewer to display
 * runtime information panels for your application.
 * --------------------------------------------------------------------------------
 */

/* Logger internal debug flags */
// Set to 0 to disable internal debug completely
// Set to 1 to activate console logs when running the logger itself
// Set to 2 to see every logging call issued by the app, too
#define LOGGER_DEBUG 0
#ifdef NSLog
	#undef NSLog
#endif

// Internal debugging stuff for the logger itself
#if LOGGER_DEBUG
	#define LOGGERDBG LoggerDbg
	#if LOGGER_DEBUG > 1
		#define LOGGERDBG2 LoggerDbg
	#else
		#define LOGGERDBG2(format, ...) do{}while(0)
	#endif
	// Internal logging function prototype
	static void LoggerDbg(CFStringRef format, ...);
#else
	#define LOGGERDBG(format, ...) do{}while(0)
	#define LOGGERDBG2(format, ...) do{}while(0)
#endif

#if defined(__has_feature) && __has_feature(objc_arc)
//#error LoggerClinet.m must be compiled without Objective-C Automatic Reference Counting (CLANG_ENABLE_OBJC_ARC=NO)
#endif


NSNetServiceBrowser *bonjourDomainBrowser;      // Domain browser

struct Logger
{
	CFStringRef bufferFile;                         // If non-NULL, all buffering is done to the specified file instead of in-memory
	CFStringRef host;                               // Viewer host to connect to (instead of using Bonjour)
	UInt32 port;                                    // port on the viewer host
	
	CFMutableArrayRef bonjourServiceBrowsers;       // Active service browsers
	CFMutableArrayRef bonjourServices;              // Services being tried
	CFMutableArrayRef logQueue;                     // Message queue
	pthread_mutex_t logQueueMutex;					// A mutex we use to protect access to the log queue and some critical variables
	pthread_cond_t logQueueEmpty;
	
	dispatch_once_t workerThreadInit;               // Use this to ensure creation of the worker thread is ever done only once for a given logger
	pthread_t workerThread;                         // The worker thread responsible for Bonjour resolution, connection and logs transmission
	CFRunLoopSourceRef messagePushedSource;         // A message source that fires on the worker thread when messages are available for send
	CFRunLoopSourceRef bufferFileChangedSource;     // A message source that fires on the worker thread when the buffer file configuration changes
	CFRunLoopSourceRef remoteOptionsChangedSource;  // A message source that fires when option changes imply a networking strategy change (switch to/from Bonjour, direct host or file streaming)
	
	CFWriteStreamRef logStream;                     // The connected stream we're writing to
	CFWriteStreamRef bufferWriteStream;             // If bufferFile not NULL and we're not connected, points to a stream for writing log data
	CFReadStreamRef bufferReadStream;               // If bufferFile not NULL, points to a read stream that will be emptied prior to sending the rest of in-memory messages
	
	SCNetworkReachabilityRef reachability;          // The reachability object we use to determine when the target host becomes reachable
	SCNetworkReachabilityFlags reachabilityFlags;   // Last known reachability flags - we use these to detect network transitions without network loss
	CFRunLoopTimerRef reconnectTimer;               // A timer to regularly check connection to the defined host, along with reachability for added reliability
	
	uint8_t *sendBuffer;                            // data waiting to be sent
	NSUInteger sendBufferSize;
	NSUInteger sendBufferUsed;                      // number of bytes of the send buffer currently in use
	NSUInteger sendBufferOffset;                    // offset in sendBuffer to start sending at
	
	int32_t messageSeq;                             // sequential message number (added to each message sent)
	
	// settings
	uint32_t options;                               // Flags, see enum above
	CFStringRef bonjourServiceType;                 // leave NULL to use the default
	CFStringRef bonjourServiceName;                 // leave NULL to use the first one available
	
	// internal state
	BOOL targetReachable;                           // Set to YES when the Reachability target (host or internet) is deemed reachable
	BOOL connected;                                 // Set to YES once the write stream declares the connection open
	volatile BOOL quit;                             // Set to YES to terminate the logger worker thread's runloop
	BOOL incompleteSendOfFirstItem;                 // set to YES if we are sending the first item in the queue and it's bigger than what the buffer can hold
};

/* Local prototypes */
static void LoggerFlushAllOnExit(void);
static void* LoggerWorkerThread(Logger *logger);
static void LoggerWriteMoreData(Logger *logger);
static void LoggerPushMessageToQueue(Logger *logger, CFDataRef message);

// Bonjour management
static void LoggerStartBonjourBrowsing(Logger *logger);
static void LoggerStopBonjourBrowsing(Logger *logger);
static void LoggerBrowseBonjourForServices(Logger *logger, CFStringRef domainName);
static void LoggerConnectToService(Logger *logger, NSNetService *service);
static void LoggerDisconnectFromService(Logger *logger, NSNetService *service);

@interface FPLLoggerBonjourDelegate : NSObject <NSNetServiceBrowserDelegate>
- (instancetype)initWithLogger:(Logger *)logger;
@end

// Reachability and reconnect timer
static void LoggerRemoteSettingsChanged(Logger *logger);
static void LoggerStartReachabilityChecking(Logger *logger);
static void LoggerStopReachabilityChecking(Logger *logger);
static void LoggerReachabilityCallBack(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info);
static void LoggerStartReconnectTimer(Logger *logger);
static void LoggerStopReconnectTimer(Logger *logger);
static void LoggerTimedReconnectCallback(CFRunLoopTimerRef timer, void *info);

// Connection & stream management
static void LoggerTryConnect(Logger *logger);
static void LoggerWriteStreamTerminated(Logger *logger);
static void LoggerWriteStreamCallback(CFWriteStreamRef ws, CFStreamEventType event, void* info);

// File buffering
static void LoggerCreateBufferWriteStream(Logger *logger);
static void LoggerCreateBufferReadStream(Logger *logger);
static void LoggerEmptyBufferFile(Logger *logger);
static void LoggerFileBufferingOptionsChanged(Logger *logger);
static void LoggerFlushQueueToBufferStream(Logger *logger, BOOL firstEntryIsClientInfo);

// Encoding functions
static void	LoggerPushClientInfoToFrontOfQueue(Logger *logger);
static void LoggerMessageAddTimestampAndThreadID(CFMutableDataRef encoder);

static CFMutableDataRef LoggerMessageCreate(int32_t seq);
static void LoggerMessageAddInt32(CFMutableDataRef encoder, int32_t anInt, int key);
#if __LP64__
static void LoggerMessageAddInt64(CFMutableDataRef data, int64_t anInt, int key);
#endif
static void LoggerMessageAddString(CFMutableDataRef encoder, CFStringRef aString, int key);
static void LoggerMessageAddData(CFMutableDataRef encoder, CFDataRef theData, int key, int partType);
static uint32_t LoggerMessageGetSeq(CFDataRef message);

/* Static objects */
static CFMutableArrayRef sLoggersList;
static Logger* volatile sDefaultLogger = NULL;
static Boolean sAtexitFunctionSet = FALSE;
static pthread_mutex_t sLoggersListMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t sDefaultLoggerMutex = PTHREAD_MUTEX_INITIALIZER;

// Console logging
static void LoggerStartGrabbingConsole(Logger *logger);
static void LoggerStopGrabbingConsole(Logger *logger);
static Logger ** consoleGrabbersList = NULL;
static unsigned consoleGrabbersListLength = 0;
static unsigned numActiveConsoleGrabbers = 0;
static pthread_mutex_t consoleGrabbersMutex = PTHREAD_MUTEX_INITIALIZER;
static int sConsolePipes[4] = { -1, -1, -1, -1 };
static int sSTDOUT = -1, sSTDERR = -1;
static int sSTDOUThadSIGPIPE, sSTDERRhadSIGPIPE;
static pthread_t consoleGrabThread;

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Default logger
// -----------------------------------------------------------------------------
void LoggerSetDefaultLogger(Logger *defaultLogger)
{
	pthread_mutex_lock(&sDefaultLoggerMutex);
	sDefaultLogger = defaultLogger;
	pthread_mutex_unlock(&sDefaultLoggerMutex);
}

Logger *LoggerGetDefaultLogger(void)
{
	Logger *l = sDefaultLogger;
	if (l == NULL)
	{
		pthread_mutex_lock(&sDefaultLoggerMutex);
		l = sDefaultLogger;
		if (l == NULL)
			l = LoggerInit();
		pthread_mutex_unlock(&sDefaultLoggerMutex);
	}
	return l;
}

Logger *LoggerCheckDefaultLogger(void)
{
	return sDefaultLogger;
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Initialization and setup
// -----------------------------------------------------------------------------
Logger *LoggerInit(void)
{
	LOGGERDBG(CFSTR("LoggerInit defaultLogger=%p"), sDefaultLogger);
	
    static Logger gLogger;
	Logger *logger = &gLogger; //! @todo Hack to avoid heap allocation
    //Logger* logger = (Logger *)malloc(sizeof(Logger));
	bzero(logger, sizeof(Logger));

	logger->logQueue = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
	pthread_mutex_init(&logger->logQueueMutex, NULL);
	pthread_cond_init(&logger->logQueueEmpty, NULL);

	logger->bonjourServiceBrowsers = CFArrayCreateMutable(NULL, 4, &kCFTypeArrayCallBacks);
	logger->bonjourServices = CFArrayCreateMutable(NULL, 4, &kCFTypeArrayCallBacks);

	// for now we don't grow the send buffer, just use one page of memory which should be enouh
	// (bigger messages will be sent separately)
	logger->sendBuffer = (uint8_t *)malloc(4096);
	logger->sendBufferSize = 4096;
	
	logger->options = LOGGER_DEFAULT_OPTIONS;
#if LOGGER_DEBUG
	// when debugging NSLogger itself, don't hijack the system console
	// as we are sending messages to it for display
	logger->options &= ~kLoggerOption_CaptureSystemConsole;
#endif

	logger->quit = NO;

	// Add logger to the list of existing loggers
	// Set this logger as the default logger is none exist already
	pthread_mutex_lock(&sLoggersListMutex);
	if (sLoggersList == NULL)
	{
		CFArrayCallBacks callbacks;
		bzero(&callbacks, sizeof(callbacks));
		sLoggersList = CFArrayCreateMutable(NULL, 0, &callbacks);
	}
	CFArrayAppendValue(sLoggersList, (const void *)logger);
	if (sDefaultLogger == NULL)
		sDefaultLogger = logger;

	// Configure a low level exit() callback that will flush all connected loggers
	if (!sAtexitFunctionSet)
	{
		atexit(&LoggerFlushAllOnExit);
		sAtexitFunctionSet = TRUE;
	}
	pthread_mutex_unlock(&sLoggersListMutex);

	return logger;
}

void LoggerSetOptions(Logger *logger, uint32_t options)
{
	LOGGERDBG(CFSTR("LoggerSetOptions options=0x%08lx"), options);

	// If we choose to log to system console
	// make sure we are not configured to capture the system console
	// When debugging NSLogger itself, we never capture the system console either
	if (options & kLoggerOption_LogToConsole)
		options &= (uint32_t)~kLoggerOption_CaptureSystemConsole;

	if (logger == NULL)
		logger = LoggerGetDefaultLogger();
	if (logger != NULL)
		logger->options = options;
}

uint32_t LoggerGetOptions(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	return logger ? logger->options : 0;
}

void LoggerSetupBonjour(Logger *logger, CFStringRef bonjourServiceType, CFStringRef bonjourServiceName)
{
	LOGGERDBG(CFSTR("LoggerSetupBonjour serviceType=%@ serviceName=%@"), bonjourServiceType, bonjourServiceName);

	if (logger == NULL)
		logger = LoggerGetDefaultLogger();
	if (logger != NULL)
	{
		pthread_mutex_lock(&logger->logQueueMutex);
		
		BOOL change = ((bonjourServiceName != NULL) != (logger->bonjourServiceName != NULL) ||
					   (bonjourServiceName != NULL && CFStringCompare(bonjourServiceName, logger->bonjourServiceName, 0) != kCFCompareEqualTo))
		||			((bonjourServiceType != NULL) != (logger->bonjourServiceType != NULL) ||
					 (bonjourServiceType != NULL && CFStringCompare(bonjourServiceType, logger->bonjourServiceType, 0) != kCFCompareEqualTo));
		
		if (bonjourServiceType != NULL)
			CFRetain(bonjourServiceType);
		if (bonjourServiceName != NULL)
			CFRetain(bonjourServiceName);
		
		if (logger->bonjourServiceType != NULL)
			CFRelease(logger->bonjourServiceType);
		if (logger->bonjourServiceName != NULL)
			CFRelease(logger->bonjourServiceName);
		
		logger->bonjourServiceType = bonjourServiceType;
		logger->bonjourServiceName = bonjourServiceName;

		if (change && logger->remoteOptionsChangedSource != NULL)
			CFRunLoopSourceSignal(logger->remoteOptionsChangedSource);
		
		pthread_mutex_unlock(&logger->logQueueMutex);
	}
}

CFStringRef LoggerGetBonjourServiceType(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	CFStringRef result = logger ? logger->bonjourServiceType : NULL;
	pthread_mutex_unlock(&logger->logQueueMutex);
	return result;
}

CFStringRef LoggerGetBonjourServiceName(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	CFStringRef result = logger ? logger->bonjourServiceName : NULL;
	pthread_mutex_unlock(&logger->logQueueMutex);
	return result;
}

/// Stringification, see this:
/// http://gcc.gnu.org/onlinedocs/cpp/Stringification.html
#define nslogger_xstr(s) nslogger_str(s)
#define nslogger_str(s) #s

void LoggerSetupBonjourForBuildUser()
{
    LoggerSetupBonjour(LoggerGetDefaultLogger(), NULL, CFSTR(nslogger_xstr(NSLOGGER_BUILD_USERNAME)));
}

void LoggerSetViewerHost(Logger *logger, CFStringRef hostName, UInt32 port)
{
	logger = logger ?: LoggerGetDefaultLogger();

	pthread_mutex_lock(&logger->logQueueMutex);
	
	CFStringRef previousHost = logger->host;
	UInt32 previousPort = logger->port;

	logger->host = NULL;

	if (hostName != NULL)
	{
		logger->host = CFStringCreateCopy(NULL, hostName);
		logger->port = port;
	}

	if (logger->remoteOptionsChangedSource != NULL &&
		(logger->port != previousPort ||
		 ((hostName == NULL) != (previousHost == NULL)) ||
		 (hostName != NULL && CFStringCompare(hostName, previousHost, kCFCompareCaseInsensitive) != kCFCompareEqualTo)))
		CFRunLoopSourceSignal(logger->remoteOptionsChangedSource);
		 
	if (previousHost != NULL)
		CFRelease(previousHost);

	pthread_mutex_unlock(&logger->logQueueMutex);
}

CFStringRef LoggerGetViewerHostName(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	CFStringRef result = logger ? logger->host : NULL;
	pthread_mutex_unlock(&logger->logQueueMutex);
	return result;
}

UInt32 LoggerGetViewerPort(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	UInt32 result = logger ? logger->port : 0;
	pthread_mutex_unlock(&logger->logQueueMutex);
	return result;
}

void LoggerSetBufferFile(Logger *logger, CFStringRef absolutePath)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	
	BOOL change = ((logger->bufferFile != NULL && absolutePath == NULL) ||
				   (logger->bufferFile == NULL && absolutePath != NULL) ||
				   (logger->bufferFile != NULL && absolutePath != NULL && CFStringCompare(logger->bufferFile, absolutePath, (CFStringCompareFlags) 0) != kCFCompareEqualTo));
	if (change)
	{
		if (logger->bufferFile != NULL)
		{
			CFRelease(logger->bufferFile);
			logger->bufferFile = NULL;
		}
		if (absolutePath != NULL)
			logger->bufferFile = CFStringCreateCopy(NULL, absolutePath);

		if (logger->bufferFileChangedSource != NULL)
			CFRunLoopSourceSignal(logger->bufferFileChangedSource);
	}

	pthread_mutex_unlock(&logger->logQueueMutex);
}

CFStringRef LoggerGetBufferFile(Logger *logger)
{
	logger = logger ?: LoggerGetDefaultLogger();
	pthread_mutex_lock(&logger->logQueueMutex);
	CFStringRef result = logger ? logger->bufferFile : NULL;
	pthread_mutex_unlock(&logger->logQueueMutex);
	return result;
}

Logger *LoggerStart(Logger *logger)
{
	// will do nothing if logger is already started
	if (logger == NULL)
		logger = LoggerGetDefaultLogger();

    if (logger != NULL)
	{
		if (logger->workerThread == NULL)
        {
			dispatch_once(&logger->workerThreadInit, ^{
				// Start the work thread which performs the Bonjour search,
				// connects to the logging service and forwards the logs
				LOGGERDBG(CFSTR("LoggerStart logger=%p"), logger);
				pthread_create(&logger->workerThread, NULL, (void *(*)(void *))&LoggerWorkerThread, logger);
				
				// Grab console output if required
				if (logger->options & kLoggerOption_CaptureSystemConsole)
					LoggerStartGrabbingConsole(logger);
			});
        }
    }
    else
    {
        LOGGERDBG2(CFSTR("-> could not create logger"));
    }
	return logger;
}

void LoggerStop(Logger *logger)
{
	LOGGERDBG(CFSTR("LoggerStop"));

	pthread_mutex_lock(&sLoggersListMutex);
	if (logger == NULL || logger == sDefaultLogger)
	{
		logger = sDefaultLogger;
		sDefaultLogger = NULL;
	}
	if (sLoggersList != NULL && logger != NULL)
	{
		CFIndex where = CFArrayGetFirstIndexOfValue(sLoggersList, CFRangeMake(0, CFArrayGetCount(sLoggersList)), (void const *) logger);
		if (where != -1)
			CFArrayRemoveValueAtIndex(sLoggersList, where);
	}
	pthread_mutex_unlock(&sLoggersListMutex);

	if (logger != NULL)
	{
		if (logger->workerThread != NULL)
		{
            LoggerStopGrabbingConsole(logger);
			logger->quit = YES;
			pthread_join(logger->workerThread, NULL);
			logger->workerThread = NULL;
		}

		CFRelease(logger->bonjourServiceBrowsers);
		CFRelease(logger->bonjourServices);
		free(logger->sendBuffer);
		if (logger->host != NULL)
			CFRelease(logger->host);
		if (logger->bufferFile != NULL)
			CFRelease(logger->bufferFile);
		if (logger->bonjourServiceType != NULL)
			CFRelease(logger->bonjourServiceType);
		if (logger->bonjourServiceName != NULL)
			CFRelease(logger->bonjourServiceName);

		// to make sure potential errors are caught, set the whole structure
		// to a value that will make code crash if it tries using pointers to it.
		memset(logger, 0x55, sizeof(Logger));

		free(logger);
	}
}

static void LoggerFlushAllOnExit()
{
	// this function is automatically configured by NSLogger to flush all connected loggers
	// on exit. this guarantees that the developer sees the last messages issued by the application.
	// it is configured the first time a logger is initialized, so at the time we're being called
	// the loggers list is never NULL
	pthread_mutex_lock(&sLoggersListMutex);
	CFIndex numLoggers = CFArrayGetCount(sLoggersList);
	for (CFIndex i=0; i < numLoggers; i++)
		LoggerFlush((Logger *) CFArrayGetValueAtIndex(sLoggersList, i), NO);
	pthread_mutex_unlock(&sLoggersListMutex);
}

void LoggerFlush(Logger *logger, BOOL waitForConnection)
{
	// Special case: if nothing has ever been logged, don't bother
	if (logger == NULL && sDefaultLogger == NULL)
		return;
	if (logger == NULL)
		logger = LoggerGetDefaultLogger();
	if (logger != NULL &&
		pthread_self() != logger->workerThread &&
		(logger->connected || logger->bufferFile != NULL || waitForConnection))		//! @todo change this test
	{
		pthread_mutex_lock(&logger->logQueueMutex);
		if (CFArrayGetCount(logger->logQueue) > 0)
			pthread_cond_wait(&logger->logQueueEmpty, &logger->logQueueMutex);
		pthread_mutex_unlock(&logger->logQueueMutex);
	}
}

#if LOGGER_DEBUG
static void LoggerDbg(CFStringRef format, ...)
{
	// Internal debugging function
	// (what do you think, that we use the Logger to debug itself ??)
	if (format != NULL)
	{
		@autoreleasepool
		{
			va_list args;
			va_start(args, format);
			CFStringRef s = CFStringCreateWithFormatAndArguments(NULL, NULL, format, args);
			va_end(args);
			if (s != NULL)
			{
				CFShow(s);
				CFRelease(s);
			}
		}
	}
}
#endif

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Main processing
// -----------------------------------------------------------------------------
static BOOL LoggerPrepareRunloopSource(Logger *logger, CFRunLoopSourceRef *outRef, void *callback)
{
	// first call will also create the thread's runloop
	CFRunLoopSourceContext context;
	bzero(&context, sizeof(context));
	context.info = logger;
	context.perform = callback;
	*outRef = CFRunLoopSourceCreate(NULL, 0, &context);
	if (*outRef == NULL)
	{
		// This NSLog is intentional as this failure MUST be logged to console
		NSLog(@"*** NSLogger: worker thread failed creating runloop source");
		return NO;
	}
	CFRunLoopAddSource(CFRunLoopGetCurrent(), *outRef, kCFRunLoopDefaultMode);
	return YES;
}

static void LoggerDisposeRunloopSource(CFRunLoopSourceRef *sourceRef)
{
	if (*sourceRef != NULL)
	{
		CFRunLoopSourceInvalidate(*sourceRef);
		CFRelease(*sourceRef);
		*sourceRef = NULL;
	}
}

static void *LoggerWorkerThread(Logger *logger)
{
	LOGGERDBG(CFSTR("Start LoggerWorkerThread"));

#if !TARGET_OS_IPHONE
	// Register thread with Garbage Collector on Mac OS X if we're running an OS version that has GC
    void (*registerThreadWithCollector_fn)(void);
    registerThreadWithCollector_fn = (void(*)(void)) dlsym(RTLD_NEXT, "objc_registerThreadWithCollector");
    if (registerThreadWithCollector_fn)
        (*registerThreadWithCollector_fn)();
#endif

	// access to the runloop sources is protected by our logQueue mutex
	pthread_mutex_lock(&logger->logQueueMutex);

	// Create the run loop source that signals when messages have been added to the runloop
	// this will directly trigger a WriteMoreData() call, which will or won't write depending
	// on whether we're connected and there's space available in the stream
	if (!LoggerPrepareRunloopSource(logger, &logger->messagePushedSource, &LoggerWriteMoreData))
	{
		// Failing to create the runloop source for pushing messages is a major failure.
		// This NSLog is intentional. We WANT console output in this case
		NSLog(@"*** NSLogger: switching to console logging.");
		logger->options |= kLoggerOption_LogToConsole;
		logger->workerThread = NULL;
		return NULL;
	}

	// Create the runloop source that lets us know when file buffering options change
	LoggerPrepareRunloopSource(logger, &logger->bufferFileChangedSource, &LoggerFileBufferingOptionsChanged);

	// Create the runloop source that lets us know when remote (host, Bonjour) settings change
	LoggerPrepareRunloopSource(logger, &logger->remoteOptionsChangedSource, &LoggerRemoteSettingsChanged);

	pthread_mutex_unlock(&logger->logQueueMutex);

	// Create the buffering stream if needed
	if (logger->bufferFile != NULL)
		LoggerCreateBufferWriteStream(logger);

	// Start Reachability (when needed), which determines when we take the next step
	// (once Reachability status is known, we'll decide to either start Bonjour browsing or
	// try connecting to a direct host)
	LoggerStartReachabilityChecking(logger);

	// Run logging thread until LoggerStop() is called
	NSTimeInterval timeout = 0.10;
	while (!logger->quit)
	{
		int result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
		if (result == kCFRunLoopRunFinished || result == kCFRunLoopRunStopped)
			break;
		if (result == kCFRunLoopRunHandledSource)
		{
			timeout = 0.0;
			continue;
		}
		timeout = fmax(1.0, fmin(0.10, timeout+0.0005));
	}

	// Cleanup
	LoggerStopBonjourBrowsing(logger);
	LoggerStopReachabilityChecking(logger);
	LoggerStopReconnectTimer(logger);

	if (logger->logStream != NULL)
	{
		CFWriteStreamSetClient(logger->logStream, 0, NULL, NULL);
		CFWriteStreamClose(logger->logStream);
		CFRelease(logger->logStream);
		logger->logStream = NULL;
	}

	if (logger->bufferWriteStream == NULL && logger->bufferFile != NULL)
	{
		// If there are messages in the queue and LoggerStop() was called and
		// a buffer file was set just before LoggerStop() was called, flush
		// the log queue to the buffer file
		pthread_mutex_lock(&logger->logQueueMutex);
		CFIndex outstandingMessages = CFArrayGetCount(logger->logQueue);
		pthread_mutex_unlock(&logger->logQueueMutex);
		if (outstandingMessages)
			LoggerCreateBufferWriteStream(logger);
	}

	if (logger->bufferWriteStream != NULL)
	{
		CFWriteStreamClose(logger->bufferWriteStream);
		CFRelease(logger->bufferWriteStream);
		logger->bufferWriteStream = NULL;
	}

	LoggerDisposeRunloopSource(&logger->messagePushedSource);
	LoggerDisposeRunloopSource(&logger->bufferFileChangedSource);
	LoggerDisposeRunloopSource(&logger->remoteOptionsChangedSource);

	// if the client ever tries to log again against us, make sure that logs at least
	// go to console
	logger->options |= kLoggerOption_LogToConsole;
	
	LOGGERDBG(CFSTR("Stop LoggerWorkerThread"));
	return NULL;
}

static CFStringRef LoggerCreateStringRepresentationFromBinaryData(CFDataRef data)
{
	CFMutableStringRef s = CFStringCreateMutable(NULL, 0);
	unsigned int offset = 0;
	unsigned int dataLen = (unsigned int)CFDataGetLength(data);
	char buffer[1+6+16*3+1+16+1+1+1];
	buffer[0] = '\0';
	const unsigned char *q = (unsigned char *)CFDataGetBytePtr(data);
	if (dataLen == 1)
		CFStringAppend(s, CFSTR("Raw data, 1 byte:\n"));
	else
		CFStringAppendFormat(s, NULL, CFSTR("Raw data, %u bytes:\n"), dataLen);
	while (dataLen)
	{
		int i, j, b = sprintf(buffer," %04x: ", offset);
		for (i=0; i < 16 && i < (int)dataLen; i++)
			sprintf(&buffer[b+3*i], "%02x ", (int)q[i]);
		for (j=i; j < 16; j++)
			strncat(buffer, "   ", 3);
		
		b = (int)strlen(buffer);
		buffer[b++] = '\'';
		for (i=0; i < 16 && i < (int)dataLen; i++, q++)
		{
			if (*q >= 32 && *q < 128)
				buffer[b++] = (char)*q;
			else
				buffer[b++] = ' ';
		}
		for (j=i; j < 16; j++)
			buffer[b++] = ' ';
		buffer[b++] = '\'';
		buffer[b++] = '\n';
		buffer[b] = 0;
		
		CFStringRef bufferStr = CFStringCreateWithBytesNoCopy(NULL, (const UInt8 *)buffer, (CFIndex)strlen(buffer), kCFStringEncodingISOLatin1, false, kCFAllocatorNull);
		CFStringAppend(s, bufferStr);
		CFRelease(bufferStr);
		
		dataLen -= (unsigned int)i;
		offset += (unsigned int)i;
	}
	return s;
}

static void LoggerLogToConsole(CFDataRef data)
{
	// Decode and log a message to the console. Doing this from the worker thread
	// allow us to serialize logging, which is a benefit that NSLog() doesn't have.
	// Only drawback is that we have to decode our own message, but that is a minor hassle.
	if (data == NULL)
	{
		CFShow(CFSTR("LoggerLogToConsole: data is NULL"));
		return;
	}
	struct timeval timestamp;
	bzero(&timestamp, sizeof(timestamp));
	int type = LOGMSG_TYPE_LOG, contentsType = PART_TYPE_STRING;
	int imgWidth=0, imgHeight=0;
	CFStringRef message = NULL;
	CFStringRef thread = NULL;

	// decode message contents
	uint8_t *p = (uint8_t *)CFDataGetBytePtr(data) + 4;
	uint16_t partCount;
	memcpy(&partCount, p, 2);
	partCount = ntohs(partCount);
	p += 2;
	while (partCount--)
	{
		uint8_t partKey = *p++;
		uint8_t partType = *p++;
		uint32_t partSize;
		if (partType == PART_TYPE_INT16)
			partSize = 2;
		else if (partType == PART_TYPE_INT32)
			partSize = 4;
		else if (partType == PART_TYPE_INT64)
			partSize = 8;
		else
		{
			memcpy(&partSize, p, 4);
			p += 4;
			partSize = ntohl(partSize);
		}
		CFTypeRef part = NULL;
		uint32_t value32 = 0;
		uint64_t value64 = 0;
		if (partSize > 0)
		{
			if (partType == PART_TYPE_STRING)
			{
				// trim whitespace and newline at both ends of the string
				uint8_t *q = p;
				uint32_t l = partSize;
				while (l && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r'))
                {
					q++;
                    l--;
                }
				uint8_t *r = q + l - 1;
				while (l && (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r'))
                {
					r--;
                    l--;
                }
				part = CFStringCreateWithBytesNoCopy(NULL, q, (CFIndex)l, kCFStringEncodingUTF8, false, kCFAllocatorNull);
			}
			else if (partType == PART_TYPE_BINARY)
			{
				part = CFDataCreateWithBytesNoCopy(NULL, p, (CFIndex)partSize, kCFAllocatorNull);
			}
			else if (partType == PART_TYPE_IMAGE)
			{
				// ignore image data, we can't log it to console
			}
			else if (partType == PART_TYPE_INT16)
			{
				value32 = ((uint32_t)p[0]) << 8 | (uint32_t)p[1];
			}
			else if (partType == PART_TYPE_INT32)
			{
				memcpy(&value32, p, 4);
				value32 = ntohl(value32);
			}
			else if (partType == PART_TYPE_INT64)
			{
				memcpy(&value64, p, 8);
				value64 = CFSwapInt64BigToHost(value64);
			}
			p += partSize;
		}
		switch (partKey)
		{
			case PART_KEY_MESSAGE_TYPE:
				type = (int)value32;
				break;
			case PART_KEY_TIMESTAMP_S:			// timestamp with seconds-level resolution
				timestamp.tv_sec = (partType == PART_TYPE_INT64) ? (__darwin_time_t)value64 : (__darwin_time_t)value32;
				break;
			case PART_KEY_TIMESTAMP_MS:			// millisecond part of the timestamp (optional)
				timestamp.tv_usec = ((partType == PART_TYPE_INT64) ? (__darwin_suseconds_t)value64 : (__darwin_suseconds_t)value32) * 1000;
				break;
			case PART_KEY_TIMESTAMP_US:			// microsecond part of the timestamp (optional)
				timestamp.tv_usec = (partType == PART_TYPE_INT64) ? (__darwin_suseconds_t)value64 : (__darwin_suseconds_t)value32;
				break;
			case PART_KEY_THREAD_ID:
				if (thread == NULL)				// useless test, we know what we're doing but clang analyzer doesn't...
				{
					if (partType == PART_TYPE_INT32)
						thread = CFStringCreateWithFormat(NULL, NULL, CFSTR("thread 0x%08x"), value32);
					else if (partType == PART_TYPE_INT64)
						thread = CFStringCreateWithFormat(NULL, NULL, CFSTR("thread 0x%qx"), value64);
					else if (partType == PART_TYPE_STRING && part != NULL)
						thread = CFRetain(part);
				}
				break;
			case PART_KEY_MESSAGE:
				if (part != NULL)
				{
					if (partType == PART_TYPE_STRING)
						message = CFRetain(part);
					else if (partType == PART_TYPE_BINARY)
						message = LoggerCreateStringRepresentationFromBinaryData(part);
				}
				contentsType = partType;
				break;
			case PART_KEY_IMAGE_WIDTH:
				imgWidth = (partType == PART_TYPE_INT32 ? (int)value32 : (int)value64);
				break;
			case PART_KEY_IMAGE_HEIGHT:
				imgHeight = (partType == PART_TYPE_INT32 ? (int)value32 : (int)value64);
				break;
			default:
				break;
		}
		if (part != NULL)
			CFRelease(part);
	}

	// Prepare the final representation and log to console
	CFMutableStringRef s = CFStringCreateMutable(NULL, 0);

	char buf[32];
	struct tm t;
	gmtime_r(&timestamp.tv_sec, &t);
	strftime(buf, sizeof(buf)-1, "%T", &t);
	CFStringRef ts = CFStringCreateWithBytesNoCopy(
                                                   NULL,
                                                   (const UInt8 *)buf,
                                                   (CFIndex)strlen(buf),
                                                   kCFStringEncodingASCII,
                                                   false,
                                                   kCFAllocatorNull);
	CFStringAppend(s, ts);
	CFRelease(ts);

	if (contentsType == PART_TYPE_IMAGE)
		message = CFStringCreateWithFormat(NULL, NULL, CFSTR("<image width=%d height=%d>"), imgWidth, imgHeight);

	buf[0] = 0;
	if (thread != NULL && CFStringGetLength(thread) < 16)
	{
		int n = 16 - (int)CFStringGetLength(thread);
		memset(buf, ' ', (size_t)n);
		buf[n] = 0;
	}
	CFStringAppendFormat(s, NULL, CFSTR(".%04d %s%@ | %@"),
						 (int)(timestamp.tv_usec / 1000),
						 buf, (thread == NULL) ? CFSTR("") : thread,
						 (message != NULL) ? message : CFSTR(""));

	if (thread != NULL)
		CFRelease(thread);
	if (message != NULL)
		CFRelease(message);

	if (type == LOGMSG_TYPE_LOG || type == LOGMSG_TYPE_MARK)
		CFShow(s);

	CFRelease(s);
}

static void LoggerWriteMoreData(Logger *logger)
{
    BOOL logToConsole = (logger->options & (kLoggerOption_LogToConsole | kLoggerOption_CaptureSystemConsole)) == kLoggerOption_LogToConsole;
	
	if (!logger->connected)
	{
		if (logToConsole)
		{
			pthread_mutex_lock(&logger->logQueueMutex);
			while (CFArrayGetCount(logger->logQueue))
			{
				LoggerLogToConsole((CFDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0));
				CFArrayRemoveValueAtIndex(logger->logQueue, 0);
			}
			pthread_mutex_unlock(&logger->logQueueMutex);
			pthread_cond_broadcast(&logger->logQueueEmpty);
		}
		else if (logger->bufferWriteStream != NULL)
		{
			LoggerFlushQueueToBufferStream(logger, NO);
		}
        else if (!(logger->options & kLoggerOption_BufferLogsUntilConnection))
        {
            /* No client connected
             * User don't want to log to console
             * User don't want to log to file
             * and user don't want us to buffer it in memory
             * So let's just sack the whole queue
             */
			pthread_mutex_lock(&logger->logQueueMutex);
			while (CFArrayGetCount(logger->logQueue))
			{
				CFArrayRemoveValueAtIndex(logger->logQueue, 0);
			}
			pthread_mutex_unlock(&logger->logQueueMutex);
			pthread_cond_broadcast(&logger->logQueueEmpty);
        }
		return;
	}
	
	if (CFWriteStreamCanAcceptBytes(logger->logStream))
	{
		// prepare archived data with log queue contents, unblock the queue as soon as possible
		CFMutableDataRef sendFirstItem = NULL;
		if (logger->sendBufferUsed == 0)
		{
			// pull more data from the log queue
			pthread_mutex_lock(&logger->logQueueMutex);
			if (logger->bufferReadStream != NULL)
			{
				if (!CFReadStreamHasBytesAvailable(logger->bufferReadStream))
				{
					CFReadStreamClose(logger->bufferReadStream);
					CFRelease(logger->bufferReadStream);
					logger->bufferReadStream = NULL;
					LoggerEmptyBufferFile(logger);
				}
				else
				{
					logger->sendBufferUsed = (NSUInteger)CFReadStreamRead(logger->bufferReadStream, logger->sendBuffer, (CFIndex)logger->sendBufferSize);
				}
			}
			else
			{
				while (CFArrayGetCount(logger->logQueue))
				{
					CFDataRef d = (CFDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0);
					CFIndex dsize = CFDataGetLength(d);
					if ((logger->sendBufferUsed + (NSUInteger)dsize) > logger->sendBufferSize)
						break;
					memcpy(logger->sendBuffer + logger->sendBufferUsed, CFDataGetBytePtr(d), (size_t)dsize);
					logger->sendBufferUsed += (NSUInteger)dsize;
					if (logToConsole)
						LoggerLogToConsole(d);
					CFArrayRemoveValueAtIndex(logger->logQueue, 0);
					logger->incompleteSendOfFirstItem = NO;
				}
			}
			if (logger->sendBufferUsed == 0) 
			{
				// are we done yet?
				if (CFArrayGetCount(logger->logQueue) == 0)
				{
					pthread_cond_broadcast(&logger->logQueueEmpty);
					pthread_mutex_unlock(&logger->logQueueMutex);
					return;
				}

				// first item is too big to fit in a single packet, send it separately
				sendFirstItem = (CFMutableDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0);
				logger->incompleteSendOfFirstItem = YES;
				logger->sendBufferOffset = 0;
			}
			pthread_mutex_unlock(&logger->logQueueMutex);
		}

		// send data over the socket. We try hard to be failsafe and if we have to send
		// data in fragments, we make sure that in case a disconnect occurs we restart
		// sending the whole message(s)
		if (logger->sendBufferUsed != 0)
		{
			CFIndex written = CFWriteStreamWrite(logger->logStream,
												 logger->sendBuffer + logger->sendBufferOffset,
												 (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset));
			if (written < 0)
			{
				// We'll get an event if the stream closes on error. Don't discard the data,
				// it will be sent as soon as a connection is re-acquired.
				LOGGERDBG(CFSTR("CFWriteStreamWrite got %d result"),written);
				return;
			}
			if ((logger->sendBufferOffset + (NSUInteger)written) < logger->sendBufferUsed)
			{
				// everything couldn't be sent at once
				logger->sendBufferOffset += (NSUInteger)written;
			}
			else
			{
				logger->sendBufferUsed = 0;
				logger->sendBufferOffset = 0;
			}
		}
		else if (sendFirstItem)
		{
			CFIndex length = CFDataGetLength(sendFirstItem) - (CFIndex)logger->sendBufferOffset;
			CFIndex written = CFWriteStreamWrite(logger->logStream,
												 CFDataGetBytePtr(sendFirstItem) + logger->sendBufferOffset,
												 length);
			if (written < 0)
			{
				// We'll get an event if the stream closes on error
				return;
			}
			if (written < length)
			{
				// The output pipe is full, and the first item has not been sent completely
				// We need to reduce the remaining data on the first item so it can be taken
				// care of at the next iteration. We take advantage of the fact that each item
				// in the queue is actually a mutable data block
				//! @todo IF WE GET DISCONNECTED WHILE DOING THIS, THINGS WILL GO WRONG - NEED TO UPDATE THIS LOGIC
				LOGGERDBG(CFSTR("Output pipe is full"));
				CFDataReplaceBytes((CFMutableDataRef)sendFirstItem, CFRangeMake(0, written), NULL, 0);
				return;
			}
			
			// we are done sending the first item in the queue, remove it now
			pthread_mutex_lock(&logger->logQueueMutex);
			CFArrayRemoveValueAtIndex(logger->logQueue, 0);
			logger->incompleteSendOfFirstItem = NO;
			logger->sendBufferOffset = 0;
			pthread_mutex_unlock(&logger->logQueueMutex);
		}
		
		pthread_mutex_lock(&logger->logQueueMutex);
		CFIndex remainingMsgs = CFArrayGetCount(logger->logQueue);
		if (remainingMsgs == 0)
			pthread_cond_broadcast(&logger->logQueueEmpty);
		pthread_mutex_unlock(&logger->logQueueMutex);
	}
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Console logs redirection support
// -----------------------------------------------------------------------------
static void LoggerLogFromConsole(CFStringRef tag, int fd, int outfd, CFMutableDataRef data)
{
	// protected by `consoleGrabbersMutex`

	const int BUFSIZE = 1000;
	size_t prognameLength = strlen(getprogname());

	UInt8 buf[BUFSIZE];
	ssize_t bytes_read = 0;
	while ((bytes_read = read(fd, buf, BUFSIZE-1)) >= 0)
	{
		if (bytes_read == 0)
			continue;

		CFDataAppendBytes(data, buf, bytes_read);

		if (outfd != -1)
		{
			// output received data to the original fd (so as to keep output in the Xcode console etc)
			//write(outfd, "##", 2); // internal debug stuff to visualize stdout/stderr fragmentation
			write(outfd, buf, (size_t)bytes_read);
		}
		
		for(;;)
		{
			// locate newline, group multiple lines in the same log string
			const unsigned char *bytes = CFDataGetMutableBytePtr(data);
			if (bytes == NULL)	// pointer may be null if data is empty
				break;
			
			NSInteger pos = 0, maxPos = CFDataGetLength(data), lineBreak = -1;
			while (pos < maxPos)
			{
				if (bytes[pos] == '\n')
					lineBreak = pos;
				pos++;
			}
			if (lineBreak == -1)
				break;
			pos = lineBreak;
			
			// detect and remove NSLog header if any
			NSUInteger offset = 0;
			if (pos > 24 + prognameLength + 2)
			{
				// "yyyy-mm-dd HH:MM:ss.SSS progname[:] "
				if ((bytes[4] == '-') && (bytes[7] == '-') && (bytes[10] == ' ') && (bytes[13] == ':') && (bytes[16] == ':') && (bytes[19] == '.'))
				{
					if ((bytes[23] == ' ') &&  (bytes[24 + prognameLength] == '['))
					{
						const char* found = strnstr((const char *)&bytes[24 + prognameLength + 1], "] ", maxPos - (24 + prognameLength + 1));
						if (found)
						{
							offset = found - (const char *)bytes + 2;
						}
					}
				}
			}
			
			// output message to grabbers
			CFStringRef message = CFStringCreateWithBytes(NULL, bytes+offset, pos-offset, kCFStringEncodingUTF8, false);
			if (message != NULL)
			{
				for (unsigned i = 0; i < consoleGrabbersListLength; i++)
				{
					if (consoleGrabbersList[i] != NULL)
						LogMessageRawToF(consoleGrabbersList[i], NULL, 0, NULL, (__bridge NSString *)tag, 0, (__bridge NSString *)message);
				}
				CFRelease(message);
			}
			else
			{
				LOGGERDBG(CFSTR("failed extracting string of length %d from fd %d"), pos-offset, fd);
			}
			
			// drop all newlines and move on
			while (++pos < maxPos && (bytes[pos] == '\n' || bytes[pos] == '\r'))
				;
			CFDataDeleteBytes(data, CFRangeMake(0, pos));
		}
	}
}

static void *LoggerConsoleGrabThread(void *context)
{
#pragma unused (context)
	pthread_mutex_lock(&consoleGrabbersMutex);

	int fdout = sConsolePipes[0];
	fcntl(fdout, F_SETFL, fcntl(fdout, F_GETFL, 0) | O_NONBLOCK);
	
	int fderr = sConsolePipes[2];
	fcntl(fderr, F_SETFL, fcntl(fderr, F_GETFL, 0) | O_NONBLOCK);
	
	CFMutableDataRef stdoutData = CFDataCreateMutable(NULL, 0);
	CFMutableDataRef stderrData = CFDataCreateMutable(NULL, 0);

	unsigned activeGrabbers = numActiveConsoleGrabbers;
	
	pthread_mutex_unlock(&consoleGrabbersMutex);
	
	while (activeGrabbers != 0)
	{
		fd_set set;
		FD_ZERO(&set);
		FD_SET(fdout, &set);
		FD_SET(fderr, &set);
		
		int ret = select(fderr + 1, &set, NULL, NULL, NULL);
		
		if (ret <= 0)
		{
			// ==0: time expired without activity
			// < 0: error occurred
			break;
		}

		pthread_mutex_lock(&consoleGrabbersMutex);
		
		activeGrabbers = numActiveConsoleGrabbers;
		if (activeGrabbers != 0)
		{
			if (FD_ISSET(fdout, &set))
				LoggerLogFromConsole(CFSTR("stdout"), fdout, sSTDOUT, stdoutData);
			if (FD_ISSET(fderr, &set ))
				LoggerLogFromConsole(CFSTR("stderr"), fderr, sSTDERR, stderrData);
		}

		pthread_mutex_unlock(&consoleGrabbersMutex);
	}
	
	CFRelease(stdoutData);
	CFRelease(stderrData);
	return NULL;
}

static void LoggerStartConsoleRedirection()
{
	// protected by `consoleGrabbersMutex`
	
	// keep the original pipes so we can still forward everything
	// (i.e. to the running IDE that needs to display or interpret console messages)
	// and remember the SIGPIPE settings, as we are going to clear them to prevent
	// the app from exiting when we close the pipes
	if (sSTDOUT == -1)
	{
		sSTDOUThadSIGPIPE = fcntl(STDOUT_FILENO, F_GETNOSIGPIPE);
		sSTDOUT = dup(STDOUT_FILENO);
		sSTDERRhadSIGPIPE = fcntl(STDERR_FILENO, F_GETNOSIGPIPE);
		sSTDERR = dup(STDERR_FILENO);
	}
	
	// create the pipes
	if (sConsolePipes[0] == -1)
	{
		if (pipe(sConsolePipes) != -1)
		{
			fcntl(sConsolePipes[0], F_SETNOSIGPIPE, 1);
			fcntl(sConsolePipes[1], F_SETNOSIGPIPE, 1);
			dup2(sConsolePipes[1], STDOUT_FILENO);
		}
	}
	
	if (sConsolePipes[2] == -1)
	{
		if (pipe(&sConsolePipes[2]) != -1)
		{
			fcntl(sConsolePipes[0], F_SETNOSIGPIPE, 1);
			fcntl(sConsolePipes[1], F_SETNOSIGPIPE, 1);
			dup2(sConsolePipes[3], STDERR_FILENO);
		}
	}
	
	pthread_create(&consoleGrabThread, NULL, &LoggerConsoleGrabThread, NULL);
}

static void LoggerStopConsoleRedirection()
{
	// protected by consoleGrabbersMutex (see below)

	// close the pipes - will force exiting the console logger thread
	// assume the console grabber mutex has been acquired
	dup2(sSTDOUT, STDOUT_FILENO);
	dup2(sSTDERR, STDERR_FILENO);
	
	close(sSTDOUT);
	close(sSTDERR);
	
	sSTDOUT = -1;
	sSTDERR = -1;
	
	// restore sigpipe flag on standard streams
	fcntl(STDOUT_FILENO, F_SETNOSIGPIPE, &sSTDOUThadSIGPIPE);
	fcntl(STDERR_FILENO, F_SETNOSIGPIPE, &sSTDERRhadSIGPIPE);
	
	// close pipes, this will trigger an error in select() and a console grab thread exit
	if (sConsolePipes[0] != -1)
	{
		close(sConsolePipes[0]);
		close(sConsolePipes[1]);
		sConsolePipes[0] = -1;
	}
	if (sConsolePipes[2] != -1)
	{
		close(sConsolePipes[2]);
		close(sConsolePipes[1]);
	}
	sConsolePipes[0] = sConsolePipes[1] = sConsolePipes[2] = sConsolePipes[3] = -1;
	numActiveConsoleGrabbers = 0;

	pthread_mutex_unlock(&consoleGrabbersMutex);
	pthread_join(consoleGrabThread, NULL);
	pthread_mutex_lock(&consoleGrabbersMutex);
}

static void LoggerStartGrabbingConsole(Logger *logger)
{
	if (!(logger->options & kLoggerOption_CaptureSystemConsole))
		return;
	
	pthread_mutex_lock(&consoleGrabbersMutex);
	
	Boolean added = FALSE;
	for (unsigned i = 0; i < numActiveConsoleGrabbers; i++)
	{
		if (consoleGrabbersList[i] == NULL)
		{
			consoleGrabbersList[i] = logger;
			numActiveConsoleGrabbers++;
			added = TRUE;
			break;
		}
	}
	if (!added)
	{
		consoleGrabbersList = realloc(consoleGrabbersList, ++consoleGrabbersListLength * sizeof(Logger *));
		consoleGrabbersList[numActiveConsoleGrabbers++] = logger;
	}
	
	LoggerStartConsoleRedirection(); // Start redirection if necessary
	
	pthread_mutex_unlock(&consoleGrabbersMutex);
}

static void LoggerStopGrabbingConsole(Logger *logger)
{
	pthread_mutex_lock(&consoleGrabbersMutex);
	
	if (numActiveConsoleGrabbers != 0)
	{
		for (unsigned grabberIndex = 0; grabberIndex < consoleGrabbersListLength; grabberIndex++)
		{
			if (consoleGrabbersList[grabberIndex] == logger)
			{
				consoleGrabbersList[grabberIndex] = NULL;
				if (--numActiveConsoleGrabbers == 0)
				{
					consoleGrabbersListLength = 0;
					free(consoleGrabbersList);
					consoleGrabbersList = NULL;
					LoggerStopConsoleRedirection();
				}
				break;
			}
		}
	}
	
	pthread_mutex_unlock(&consoleGrabbersMutex);
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark File buffering functions
// -----------------------------------------------------------------------------
static void LoggerCreateBufferWriteStream(Logger *logger)
{
	LOGGERDBG(CFSTR("LoggerCreateBufferWriteStream to file %@"), logger->bufferFile);
	CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, logger->bufferFile, kCFURLPOSIXPathStyle, false);
	if (fileURL != NULL)
	{
		// Create write stream to file
		logger->bufferWriteStream = CFWriteStreamCreateWithFile(NULL, fileURL);
		CFRelease(fileURL);
		if (logger->bufferWriteStream != NULL)
		{
			// Set flag to append new data to buffer file
			CFWriteStreamSetProperty(logger->bufferWriteStream, kCFStreamPropertyAppendToFile, kCFBooleanTrue);

			// Open the buffer stream for writing
			if (!CFWriteStreamOpen(logger->bufferWriteStream))
			{
				CFRelease(logger->bufferWriteStream);
				logger->bufferWriteStream = NULL;
			}
			else
			{
				// Write client info and flush the queue contents to buffer file
				LoggerPushClientInfoToFrontOfQueue(logger);
				LoggerFlushQueueToBufferStream(logger, YES);
			}
		}
	}
	if (logger->bufferWriteStream == NULL)
	{
		CFShow(CFSTR("NSLogger Warning: failed opening buffer file for writing:"));
		CFShow(logger->bufferFile);
	}
}

static void LoggerCreateBufferReadStream(Logger *logger)
{
	LOGGERDBG(CFSTR("LoggerCreateBufferReadStream from file %@"), logger->bufferFile);
	CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, logger->bufferFile, kCFURLPOSIXPathStyle, false);
	if (fileURL != NULL)
	{
		// Create read stream from file
		logger->bufferReadStream = CFReadStreamCreateWithFile(NULL, fileURL);
		CFRelease(fileURL);
		if (logger->bufferReadStream != NULL)
		{
			if (!CFReadStreamOpen(logger->bufferReadStream))
			{
				CFRelease(logger->bufferReadStream);
				logger->bufferReadStream = NULL;
			}
		}
	}
}

static void LoggerEmptyBufferFile(Logger *logger)
{
	// completely remove the buffer file from storage
	LOGGERDBG(CFSTR("LoggerEmptyBufferFile %@"), logger->bufferFile);
	if (logger->bufferFile != NULL)
	{
		CFIndex bufferSize = 1 + CFStringGetLength(logger->bufferFile) * 3;
		char *buffer = (char *)malloc((size_t)bufferSize);
		if (buffer != NULL)
		{
			if (CFStringGetFileSystemRepresentation(logger->bufferFile, buffer, bufferSize))
			{
				// remove file
				unlink(buffer);
			}
			free(buffer);
		}
	}
}

static void LoggerFileBufferingOptionsChanged(Logger *logger)
{
	// File buffering options changed (callback called on logger thread):
	// - close the current buffer file stream, if any
	// - create a new one, if needed
	LOGGERDBG(CFSTR("LoggerFileBufferingOptionsChanged bufferFile=%@"), logger->bufferFile);
	if (logger->bufferWriteStream != NULL)
	{
		CFWriteStreamClose(logger->bufferWriteStream);
		CFRelease(logger->bufferWriteStream);
		logger->bufferWriteStream = NULL;
	}
	if (logger->bufferFile  != NULL)
		LoggerCreateBufferWriteStream(logger);
}

static void LoggerFlushQueueToBufferStream(Logger *logger, BOOL firstEntryIsClientInfo)
{
	LOGGERDBG(CFSTR("LoggerFlushQueueToBufferStream"));
	pthread_mutex_lock(&logger->logQueueMutex);
	if (logger->incompleteSendOfFirstItem)
	{
		// drop anything being sent
		logger->sendBufferUsed = 0;
		logger->sendBufferOffset = 0;
	}
	logger->incompleteSendOfFirstItem = NO;

	// Write outstanding messages to the buffer file (streams don't detect disconnection
	// until the next write, where we could lose one or more messages)
	if (!firstEntryIsClientInfo && logger->sendBufferUsed)
		CFWriteStreamWrite(logger->bufferWriteStream, logger->sendBuffer + logger->sendBufferOffset, (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset));
	
	int n = 0;
	while (CFArrayGetCount(logger->logQueue))
	{
		CFDataRef data = CFArrayGetValueAtIndex(logger->logQueue, 0);
		CFIndex dataLength = CFDataGetLength(data);
		CFIndex written = CFWriteStreamWrite(logger->bufferWriteStream, CFDataGetBytePtr(data), dataLength);
		if (written != dataLength)
		{
			// couldn't write all data to file, maybe storage run out of space?
			CFShow(CFSTR("NSLogger Error: failed flushing the whole queue to buffer file:"));
			CFShow(logger->bufferFile);
			break;
		}
		CFArrayRemoveValueAtIndex(logger->logQueue, 0);
		if (n == 0 && firstEntryIsClientInfo && logger->sendBufferUsed)
		{
			// try hard: write any outstanding messages to the buffer file, after the client info
			CFWriteStreamWrite(logger->bufferWriteStream, logger->sendBuffer + logger->sendBufferOffset, (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset));
		}
		n++;
	}
	logger->sendBufferUsed = 0;
	logger->sendBufferOffset = 0;
	pthread_mutex_unlock(&logger->logQueueMutex);
	pthread_cond_broadcast(&logger->logQueueEmpty);
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Bonjour browsing
// -----------------------------------------------------------------------------
static void LoggerStartBonjourBrowsing(Logger *logger)
{
	if (!logger->targetReachable ||
        bonjourDomainBrowser != NULL ||
		!(logger->options & kLoggerOption_BrowseBonjour))
		return;

	LOGGERDBG(CFSTR("LoggerStartBonjourBrowsing"));
	
	if (logger->options & kLoggerOption_BrowseOnlyLocalDomain)
	{
		LOGGERDBG(CFSTR("Logger configured to search only the local domain, searching for services on: local."));
		LoggerBrowseBonjourForServices(logger, CFSTR("local."));
	}
	else
	{
		LOGGERDBG(CFSTR("Logger configured to search all domains, browsing for domains first"));
		NSNetServiceBrowser *browser = [NSNetServiceBrowser new];
        FPLLoggerBonjourDelegate *tmp = [[FPLLoggerBonjourDelegate alloc] initWithLogger:logger];
		browser.delegate = tmp;
		[browser searchForBrowsableDomains];
		bonjourDomainBrowser = browser;
	}
}

static void LoggerStopBonjourBrowsing(Logger *logger)
{
	LOGGERDBG(CFSTR("LoggerStopBonjourBrowsing"));
	
	// stop browsing for domains
	if (bonjourDomainBrowser != NULL)
	{
		[bonjourDomainBrowser stop];
//		[bonjourDomainBrowser.delegate release];
//		[bonjourDomainBrowser release];
		bonjourDomainBrowser = NULL;
	}
	
	// stop browsing for services
	CFIndex idx;
	for (idx = 0; idx < CFArrayGetCount(logger->bonjourServiceBrowsers); idx++)
	{
		NSNetServiceBrowser *browser = CFArrayGetValueAtIndex(logger->bonjourServiceBrowsers, idx);
		[browser stop];
//		[browser.delegate release];
	}
	CFArrayRemoveAllValues(logger->bonjourServiceBrowsers);
	
	// Forget all services
	CFArrayRemoveAllValues(logger->bonjourServices);
}

static void LoggerBrowseBonjourForServices(Logger *logger, CFStringRef domainName)
{
	NSNetServiceBrowser *browser;
	browser = [NSNetServiceBrowser new];
	browser.includesPeerToPeer = (logger->options & kLoggerOption_BrowsePeerToPeer) == kLoggerOption_BrowsePeerToPeer;
    FPLLoggerBonjourDelegate *tmp = [[FPLLoggerBonjourDelegate alloc] initWithLogger:logger];
	browser.delegate = tmp;

	// try to use the user-specfied service type if any, fallback on our
	// default service type
	CFStringRef serviceType = logger->bonjourServiceType;
	if (serviceType == NULL)
	{
		if (logger->options & kLoggerOption_UseSSL)
			serviceType = LOGGER_SERVICE_TYPE_SSL;
		else
			serviceType = LOGGER_SERVICE_TYPE;
	}
	
	[browser searchForServicesOfType:(__bridge NSString *)serviceType inDomain:(__bridge NSString *)domainName];
	LOGGERDBG(CFSTR("Logger started search for services of type %@ in domain %@"), serviceType, domainName);
	CFArrayAppendValue(logger->bonjourServiceBrowsers, (__bridge const void *)(browser));
//	[browser release];
}

static void LoggerConnectToService(Logger *logger, NSNetService *service)
{
	// a service has been found
	LOGGERDBG(CFSTR("Logger found service: %@"), service);
	if (service == NULL)
		return;
	
	// if the user has specified that Logger shall only connect to the specified
	// Bonjour service name, check it now. This makes things easier in a teamwork
	// environment where multiple instances of NSLogger viewer may run on the
	// same network
	CFStringRef serviceName = (__bridge CFStringRef)service.name;
	if (logger->bonjourServiceName != NULL)
	{
		LOGGERDBG(CFSTR("-> looking for services of name %@"), logger->bonjourServiceName);
		if (serviceName == NULL || kCFCompareEqualTo != CFStringCompare(serviceName, logger->bonjourServiceName, kCFCompareCaseInsensitive | kCFCompareDiacriticInsensitive))
		{
			LOGGERDBG(CFSTR("-> service name %@ does not match requested service name, ignoring."), serviceName);
			return;
		}
	}
	else
	{
		// If the desktop viewer we found requested that only clients looking for its name can connect,
		// honor the request and do not connect. This helps with teams having multiple devices and multiple
		// desktops with NSLogger installed to avoid unwanted logs coming to a specific viewer
		// To indicate that the desktop only wants clients that are looking for its specific name,
		// the desktop sets the TXT record to be a dictionary containing the @"filterClients" key with value @"1"
		CFDataRef txtData = (__bridge CFDataRef)service.TXTRecordData;
		if (txtData != NULL)
		{
			CFDictionaryRef txtDict = CFNetServiceCreateDictionaryWithTXTData(NULL, txtData);
			if (txtDict != NULL)
			{
				const void *value = CFDictionaryGetValue(txtDict, CFSTR("filterClients"));
				Boolean mismatch = (value != NULL &&
									CFGetTypeID((CFTypeRef)value) == CFStringGetTypeID() &&
									CFStringCompare((CFStringRef)value, CFSTR("1"), 0) != kCFCompareEqualTo);
				CFRelease(txtDict);
				if (mismatch)
				{
					LOGGERDBG(CFSTR("-> service %@ requested that only clients looking for it do connect."), serviceName);
					return;
				}
			}
		}
	}
	CFArrayAppendValue(logger->bonjourServices, (__bridge const void *)(service));
	LoggerTryConnect(logger);
}

static void LoggerDisconnectFromService(Logger *logger, NSNetService *service)
{
	CFIndex idx = CFArrayGetFirstIndexOfValue(logger->bonjourServices, CFRangeMake(0, CFArrayGetCount(logger->bonjourServices)), (__bridge const void *)(service));
	if (idx == -1)
		return;
	
	[service stop];
	
	CFArrayRemoveValueAtIndex(logger->bonjourServices, idx);
}

@implementation FPLLoggerBonjourDelegate
{
	Logger *_logger;
}

- (instancetype)initWithLogger:(Logger *)logger;
{
	if (!(self = [super init]))
		return nil;
	
	_logger = logger;
	
	return self;
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDictionary<NSString *, NSNumber *> *)errorDict
{
	LOGGERDBG(CFSTR("netServiceBrowser:%@ didNotSearch:%@"), browser, errorDict);
	
	if (browser == bonjourDomainBrowser)
	{
		// An error occurred, revert to console logging if there is no remote host
		LOGGERDBG(CFSTR("*** Logger: could not browse for domains, reverting to console logging. ***"));
		CFRelease((__bridge CFTypeRef)(bonjourDomainBrowser));
		bonjourDomainBrowser = NULL;
		if (self->_logger->host == NULL)
			self->_logger->options |= kLoggerOption_LogToConsole;
	}
	else if (self->_logger->host == NULL)
	{
		LOGGERDBG(CFSTR("*** Logger: could not browse for services, no remote host configured: reverting to console logging. ***"));
		self->_logger->options |= kLoggerOption_LogToConsole;
	}
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindDomain:(NSString *)domain moreComing:(BOOL)moreComing
{
	LoggerBrowseBonjourForServices(self->_logger, (__bridge CFStringRef)domain);
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing
{
	LoggerConnectToService(self->_logger, service);
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing
{
	LoggerDisconnectFromService(self->_logger, service);
}

@end

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Reachability & Connectivity Management
// -----------------------------------------------------------------------------
static void LoggerRemoteSettingsChanged(Logger *logger)
{
	// this is a callback for a runloop source, called on the logger thread
	
	// Always terminate any ongoing connection first
	LoggerWriteStreamTerminated(logger);

	LoggerStopBonjourBrowsing(logger);
	LoggerStopReconnectTimer(logger);
	LoggerStopReachabilityChecking(logger);

	if (logger->host == NULL && !(logger->options & kLoggerOption_BrowseBonjour))
	{
		// developer doesn't want any network connection
	}
	else
	{
		// we may already have Reachability or Bonjour browsing running,
		// the calls do nothing if they are not needed
		LoggerStartReachabilityChecking(logger);
		if (logger->targetReachable)
		{
			if (logger->options & kLoggerOption_BrowseBonjour)
				LoggerStartBonjourBrowsing(logger);
		}
		LoggerTryConnect(logger);
	}
}

static void LoggerStartReachabilityChecking(Logger *logger)
{
	if (logger->reachability == NULL)
	{
		// start reachability only when network will be required, which is at least one of:
		// - Bonjour is being used
		// - A target host has been specified
		if (logger->host == NULL && !(logger->options & kLoggerOption_BrowseBonjour))
			return;

		if (logger->host != NULL)
		{
			// reachability targeted to the configured host
			LOGGERDBG(CFSTR("Starting SCNetworkReachability to wait for host %@ to be reachable"), logger->host);
			CFIndex length = CFStringGetLength(logger->host) * 3;
			char *buffer = (char *)malloc((size_t)length + 1);
			CFStringGetBytes(logger->host, CFRangeMake(0, CFStringGetLength(logger->host)), kCFStringEncodingUTF8, '?', false, (UInt8 *)buffer, length, &length);
			buffer[length] = 0;

			logger->reachability = SCNetworkReachabilityCreateWithName(NULL, buffer);

			free(buffer);
		}
		else
		{
			// reachability for generic connection to the internet
			LOGGERDBG(CFSTR("Starting SCNetworkReachability to wait for internet to be reachable"));
			struct sockaddr_in addr;
			bzero(&addr, sizeof(addr));
			addr.sin_len = (__uint8_t) sizeof(addr);
			addr.sin_family = AF_INET;

			logger->reachability = SCNetworkReachabilityCreateWithAddress(NULL, (const struct sockaddr *)&addr);
		}
		
		SCNetworkReachabilityContext context = {0, logger, NULL, NULL, NULL};
		SCNetworkReachabilitySetCallback(logger->reachability, &LoggerReachabilityCallBack, &context);
		SCNetworkReachabilityScheduleWithRunLoop(logger->reachability, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);

		// arm the callback
		if (SCNetworkReachabilityGetFlags(logger->reachability, &logger->reachabilityFlags))
			LoggerReachabilityCallBack(logger->reachability, logger->reachabilityFlags, logger);
	}
}

static void LoggerStopReachabilityChecking(Logger *logger)
{
	if (logger->reachability != NULL)
	{
		LOGGERDBG(CFSTR("Stopping SCNetworkReachability"));
		SCNetworkReachabilityUnscheduleFromRunLoop(logger->reachability, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
		CFRelease((CFTypeRef)logger->reachability);
		logger->reachability = NULL;
	}
	LoggerStopReconnectTimer(logger);
	logger->targetReachable = NO;
}

static void LoggerReachabilityCallBack(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info)
{
#pragma unused (target)
	Logger *logger = (Logger *)info;

	LOGGERDBG(CFSTR("LoggerReachabilityCallBack called with flags=0x%08lx"), flags);

	SCNetworkReachabilityFlags oldFlags = logger->reachabilityFlags;
	logger->reachabilityFlags = flags;
	
	bool isReachable = flags & kSCNetworkReachabilityFlagsReachable;
	if (!isReachable && (logger->connected || logger->logStream != NULL))
	{
		// lost internet connecton. Force a disconnect, we'll wait for the connection to become
		// available again
		LOGGERDBG(CFSTR("-> target became unreachable"));
		logger->targetReachable = NO;
		if (flags != oldFlags && logger->logStream != NULL)
			LoggerWriteStreamTerminated(logger);
		LoggerStopBonjourBrowsing(logger);
		LoggerStopReconnectTimer(logger);
	}
	else
	{
		if (isReachable)
			LOGGERDBG(CFSTR("-> target became reachable"));
		else
			LOGGERDBG(CFSTR("-> target is not reachable, let's pretend it is and try to connect anyway"));
		
		logger->targetReachable = YES;
		
		// in the event a network transition occurred without network loss (i.e. WiFi -> 3G),
		// preemptively disconnect. In many cases, if the network stays up, we will never receive
		// a disconnection (possibly due to SSH ?)
		if (flags != oldFlags && logger->logStream != NULL)
			LoggerWriteStreamTerminated(logger);
		else
			LoggerTryConnect(logger);			// will start Bonjour browsing if needed
	}
}

static void LoggerStartReconnectTimer(Logger *logger)
{
	// start a timer that will try to reconnect every 5 seconds
	if (logger->reconnectTimer == NULL && (logger->host != NULL || (logger->options & kLoggerOption_BrowseBonjour)))
	{
		LOGGERDBG(CFSTR("Starting the reconnect timer"));
		CFRunLoopTimerContext timerCtx = {
			.version = 0,
			.info = logger,
			.retain = NULL,
			.release = NULL,
			.copyDescription = NULL
		};
		logger->reconnectTimer = CFRunLoopTimerCreate(NULL,
													  CFAbsoluteTimeGetCurrent() + 5,
													  5, // reconnect interval
													  0,
													  0,
													  &LoggerTimedReconnectCallback,
													  &timerCtx);
		if (logger->reconnectTimer != NULL)
		{
			LOGGERDBG(CFSTR("Starting the TimedReconnect timer to regularly retry the connection"));
			CFRunLoopAddTimer(CFRunLoopGetCurrent(), logger->reconnectTimer, kCFRunLoopCommonModes);
		}
	}
}

static void LoggerStopReconnectTimer(Logger *logger)
{
	if (logger->reconnectTimer != NULL)
	{
		LOGGERDBG(CFSTR("Stopping the reconnect timer"));
		CFRunLoopTimerInvalidate(logger->reconnectTimer);
		CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), logger->reconnectTimer, kCFRunLoopCommonModes);
		CFRelease(logger->reconnectTimer);
		logger->reconnectTimer = NULL;
	}
}

static void LoggerTimedReconnectCallback(CFRunLoopTimerRef timer, void *info)
{
#pragma unused (timer)
	Logger *logger = (Logger *)info;
	assert(logger != NULL);
	LOGGERDBG(CFSTR("LoggerTimedReconnectCallback"));
	if (logger->logStream == NULL)
	{
		LOGGERDBG(CFSTR("-> trying to reconnect to host %@"), logger->host);
		LoggerTryConnect(logger);
	}
	else
	{
		LOGGERDBG(CFSTR("-> timer not needed anymore, removing it form runloop"));
		LoggerStopReconnectTimer(logger);
	}
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Stream management
// -----------------------------------------------------------------------------
static BOOL LoggerConfigureAndOpenStream(Logger *logger)
{
	// configure and open stream
	LOGGERDBG(CFSTR("LoggerConfigureAndOpenStream configuring and opening log stream"));
	CFStreamClientContext context = {0, (void *)logger, NULL, NULL, NULL};
	if (CFWriteStreamSetClient(logger->logStream,
							   (kCFStreamEventOpenCompleted |
								kCFStreamEventCanAcceptBytes |
								kCFStreamEventErrorOccurred |
								kCFStreamEventEndEncountered),
							   &LoggerWriteStreamCallback,
							   &context))
	{
		if (logger->options & kLoggerOption_UseSSL)
		{
			// Configure stream to require a SSL connection
			LOGGERDBG(CFSTR("-> configuring SSL"));
			const void *SSLKeys[] = {
				kCFStreamSSLLevel,
				kCFStreamSSLValidatesCertificateChain,
				kCFStreamSSLIsServer,
				kCFStreamSSLPeerName
			};
			const void *SSLValues[] = {
				kCFStreamSocketSecurityLevelNegotiatedSSL,
				kCFBooleanFalse,			// no certificate chain validation (we use a self-signed certificate)
				kCFBooleanFalse,			// not a server
				kCFNull
			};
			
			CFDictionaryRef SSLDict = CFDictionaryCreate(NULL, SSLKeys, SSLValues, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
			CFWriteStreamSetProperty(logger->logStream, kCFStreamPropertySSLSettings, SSLDict);
			CFRelease(SSLDict);
		}

		CFWriteStreamScheduleWithRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
		
		if (CFWriteStreamOpen(logger->logStream))
		{
			LOGGERDBG(CFSTR("-> stream open attempt, waiting for open completion"));
			return YES;
		}

		LOGGERDBG(CFSTR("-> stream open failed."));
		
		CFWriteStreamSetClient(logger->logStream, kCFStreamEventNone, NULL, NULL);
		if (CFWriteStreamGetStatus(logger->logStream) == kCFStreamStatusOpen)
			CFWriteStreamClose(logger->logStream);
		CFWriteStreamUnscheduleFromRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
	}
	else
	{
		LOGGERDBG(CFSTR("-> stream set client failed."));
	}
	CFRelease(logger->logStream);
	logger->logStream = NULL;
	return NO;
}

static void LoggerTryConnect(Logger *logger)
{
	// Core function that attempts connection to found Bonjour services and configured Host
	LOGGERDBG(CFSTR("LoggerTryConnect, %d services registered, current stream=%@"), CFArrayGetCount(logger->bonjourServices), logger->logStream);
	
	// If we already have a connection established or being attempted, stop here
	if (logger->logStream != NULL)
	{
		LOGGERDBG(CFSTR("-> another connection is opened or in progress, giving up for now"));
		return;
	}
	
	// If reachability status is not known yet, just wait
	if (logger->targetReachable == NO)
	{
		LOGGERDBG(CFSTR("-> not sure target is reachable, let's wait and see"));
		return;
	}

	// If there are discovered Bonjour services, try them now
	while (CFArrayGetCount(logger->bonjourServices))
	{
		NSNetService *service = CFArrayGetValueAtIndex(logger->bonjourServices, 0);
		LOGGERDBG(CFSTR("-> Trying to open write stream to service %@"), service);
		NSOutputStream *outputStream;
		[service getInputStream:NULL outputStream:&outputStream];
		logger->logStream = (__bridge CFWriteStreamRef)outputStream;
		CFArrayRemoveValueAtIndex(logger->bonjourServices, 0);
		if (logger->logStream == NULL)
		{
			// create pair failed
			LOGGERDBG(CFSTR("-> failed."));
		}
		else if (LoggerConfigureAndOpenStream(logger))
		{
			// open is now in progress
			return;
		}
	}

	// If there is a host to directly connect to, try it now (this will happen before
	// Bonjour kicks in, Bonjour being handled as a fallback solution if direct Host
	// fails)
	if (logger->host != NULL)
	{
		LOGGERDBG(CFSTR("-> Trying to open direct connection to host %@ port %u"), logger->host, logger->port);
		CFStreamCreatePairWithSocketToHost(NULL, logger->host, logger->port, NULL, &logger->logStream);
		if (logger->logStream == NULL)
		{
			// Create stream failed
			LOGGERDBG(CFSTR("-> failed."));
			if (logger->logStream != NULL)
			{
				CFRelease(logger->logStream);
				logger->logStream = NULL;
			}
		}
		else if (LoggerConfigureAndOpenStream(logger))
		{
			// open is now in progress
			return;
		}
		LoggerStartReconnectTimer(logger);
	}
	
	// Finally, if Bonjour is enabled and not started yet, start it now.
	if (logger->options & kLoggerOption_BrowseBonjour)
	{
		if (bonjourDomainBrowser == NULL || CFArrayGetCount(logger->bonjourServiceBrowsers) == 0)
		{
			LoggerStopBonjourBrowsing(logger);
			LoggerStartBonjourBrowsing(logger);
		}
	}
}

static void LoggerWriteStreamTerminated(Logger *logger)
{
	LOGGERDBG(CFSTR("LoggerWriteStreamTerminated called"));

	if (logger->connected)
	{
		LOGGERDBG(CFSTR("-> Logger DISCONNECTED"));
		logger->connected = NO;
	}

	if (logger->logStream != NULL)
	{
		LOGGERDBG(CFSTR("-> disposing the write stream"));
		CFWriteStreamSetClient(logger->logStream, 0, NULL, NULL);
		CFWriteStreamUnscheduleFromRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
		CFWriteStreamClose(logger->logStream);
		
		CFRelease(logger->logStream);
		logger->logStream = NULL;
	}

	if (logger->bufferReadStream != NULL)
	{
		// In the case the connection drops before we have flushed the
		// whole contents of the file, we choose to keep it integrally
		// and retransmit it when reconnecting to the viewer. The reason
		// of this choice is that we may have transmitted only part of
		// a message, and this may cause errors on the desktop side.
		LOGGERDBG(CFSTR("-> closing the bufferReadStream"));
		CFReadStreamClose(logger->bufferReadStream);
		CFRelease(logger->bufferReadStream);
		logger->bufferReadStream = NULL;
	}

	if (logger->bufferFile != NULL && logger->bufferWriteStream == NULL)
		LoggerCreateBufferWriteStream(logger);

	// ensure that any current block on LoggerFlush() gets unblocked
	pthread_cond_broadcast(&logger->logQueueEmpty);

	// tryConnect will take care of setting up the reconnect timer if needed
	if (logger->targetReachable &&
		(logger->host != NULL || (logger->options & kLoggerOption_BrowseBonjour)))
		LoggerTryConnect(logger);
}

static void LoggerWriteStreamCallback(CFWriteStreamRef ws, CFStreamEventType event, void* info)
{
	Logger *logger = (Logger *)info;
	assert(ws == logger->logStream);
	switch (event)
	{
		case kCFStreamEventOpenCompleted:
			// A stream open was complete. Cancel all bonjour browsing,
			// service resolution and connection attempts, and try to
			// write existing buffer contents
			LOGGERDBG(CFSTR("Logger CONNECTED"));
			logger->connected = YES;
			LoggerStopBonjourBrowsing(logger);
			LoggerStopReconnectTimer(logger);
			if (logger->bufferWriteStream != NULL)
			{
				// now that a connection is acquired, we can stop logging to a file
				CFWriteStreamClose(logger->bufferWriteStream);
				CFRelease(logger->bufferWriteStream);
				logger->bufferWriteStream = NULL;
			}
			if (logger->bufferFile != NULL)
			{
				// if a buffer file was defined, try to read its contents
				LoggerCreateBufferReadStream(logger);
			}
			LoggerPushClientInfoToFrontOfQueue(logger);
			LoggerWriteMoreData(logger);
			break;
			
		case kCFStreamEventCanAcceptBytes:
			LoggerWriteMoreData(logger);
			break;
			
		case kCFStreamEventErrorOccurred: {
			CFErrorRef error = CFWriteStreamCopyError(ws);
			LOGGERDBG(CFSTR("Logger stream error: %@"), error);
			CFRelease(error);
			// Fall-thru
		}
			
		case kCFStreamEventEndEncountered:
			LoggerWriteStreamTerminated(logger);
			break;

		// avoid warnings when building; cover all enum cases.
        case kCFStreamEventNone:
        case kCFStreamEventHasBytesAvailable:
            break;
	}
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Internal encoding functions
// -----------------------------------------------------------------------------
static uint8_t *LoggerMessagePrepareForPart(CFMutableDataRef encoder, uint32_t requiredExtraBytes)
{
	// Ensure a data block has the required storage capacity, update the total size and part count
	// then return a pointer for fast storage of the data
	uint8_t *p = CFDataGetMutableBytePtr(encoder);
	CFIndex size = CFDataGetLength(encoder);
	uint32_t oldSize = ntohl(*(uint32_t *)p);
	uint32_t newSize = oldSize + requiredExtraBytes;
	if ((newSize + 4) > (uint32_t)size)
	{
		// grow by 64 bytes chunks
		CFDataSetLength(encoder, (newSize + 4 + 64) & ~63);
		p = CFDataGetMutableBytePtr(encoder);
	}
	*((uint32_t *)p) = htonl(newSize);
	p += 4;
	*((uint16_t *)p) = htons(ntohs(*(uint16_t *)p) + 1);

	// return a pointer to where new data must be put
	return p + oldSize;
}

static void LoggerMessageAddTimestamp(CFMutableDataRef encoder)
{
	struct timeval t;
	if (gettimeofday(&t, NULL) == 0)
	{
#if __LP64__
		LoggerMessageAddInt64(encoder, t.tv_sec, PART_KEY_TIMESTAMP_S);
		LoggerMessageAddInt64(encoder, t.tv_usec, PART_KEY_TIMESTAMP_US);
#else
		LoggerMessageAddInt32(encoder, t.tv_sec, PART_KEY_TIMESTAMP_S);
		LoggerMessageAddInt32(encoder, t.tv_usec, PART_KEY_TIMESTAMP_US);
#endif
	}
	else
	{
		time_t ts = time(NULL);
#if __LP64__
		LoggerMessageAddInt64(encoder, ts, PART_KEY_TIMESTAMP_S);
#else
		LoggerMessageAddInt32(encoder, ts, PART_KEY_TIMESTAMP_S);
#endif
	}
}

static void LoggerMessageAddTimestampAndThreadID(CFMutableDataRef encoder)
{
	LoggerMessageAddTimestamp(encoder);

	BOOL hasThreadName = NO;
	// Getting the thread number is tedious, to say the least. Since there is
	// no direct way to get it, we have to do it sideways. Note that it can be dangerous
	// to use any Cocoa call when in a multithreaded application that only uses non-Cocoa threads
	// and for which Cocoa's multithreading has not been activated. We test for this case.
	BOOL inMainThread = [NSThread isMainThread];
	if (inMainThread)
	{
		hasThreadName = YES;
		LoggerMessageAddString(encoder, CFSTR("Main thread"), PART_KEY_THREAD_ID);
	}
	else if ([NSThread isMultiThreaded])
	{
		NSThread *thread = [NSThread currentThread];
		NSString *name = [thread name];
		if (![name length])
		{
			// use the thread dictionary to store and retrieve the computed thread name
			NSMutableDictionary *threadDict = [thread threadDictionary];
			name = [threadDict objectForKey:@"__$NSLoggerThreadName$__"];
			if (name == nil)
			{
				@autoreleasepool {
					// optimize CPU use by computing the thread name once and storing it back
					// in the thread dictionary
					name = [thread description];
                    NSArray *threadNumberPrefixes = @[@"num = ", @"number = "];
                    NSRange range = NSMakeRange(NSNotFound, 0);
                    
                    for (NSString *threadNumberPrefix in threadNumberPrefixes)
                    {
                        range = [name rangeOfString:threadNumberPrefix];
                        
                        if (range.location != NSNotFound)
                            break;
                    }
					
					if (range.location != NSNotFound)
					{
						// iOS and OS X now add a "name = (null)" when thread name unknown. Suppress it.
						NSRange noNameRange = [name rangeOfString:@", name = (null)" options:(NSLiteralSearch | NSBackwardsSearch)];
						if (noNameRange.location != NSNotFound)
						{
							name = [NSString stringWithFormat:@"Thread %@",
									[name substringWithRange:NSMakeRange(range.location + range.length,
																		 noNameRange.location - range.location - range.length)]];
						}
						else
						{
							name = [NSString stringWithFormat:@"Thread %@",
									[name substringWithRange:NSMakeRange(range.location + range.length,
																		 [name length] - range.location - range.length - 1)]];
						}
						[threadDict setObject:name forKey:@"__$NSLoggerThreadName$__"];
					}
                    else
                    {
                        name = nil;
                    }
				}
			}
		}
		if (name != nil)
		{
			LoggerMessageAddString(encoder, (__bridge CFStringRef)name, PART_KEY_THREAD_ID);
			hasThreadName = YES;
		}
	}
	if (!hasThreadName)
	{
#if __LP64__
		LoggerMessageAddInt64(encoder, (int64_t)pthread_self(), PART_KEY_THREAD_ID);
#else
		LoggerMessageAddInt32(encoder, (int32_t)pthread_self(), PART_KEY_THREAD_ID);
#endif
	}
}

static CFMutableDataRef LoggerMessageCreate(int32_t seq)
{
	CFMutableDataRef encoder = CFDataCreateMutable(NULL, 0);
	if (encoder != NULL)
	{
		CFDataIncreaseLength(encoder, 64);
		uint8_t *p = CFDataGetMutableBytePtr(encoder);
		if (p != NULL)
		{
			// directly write the sequence number as first part of the message
			// so we find it quickly when inserting new messages in the queue
			if (seq)
			{
				p[3] = 8;		// size 0x00000008 in big endian
				p[5] = 1;		// part count 0x0001
				p[6] = (uint8_t)PART_KEY_MESSAGE_SEQ;
				p[7] = (uint8_t)PART_TYPE_INT32;
				*(uint32_t *)(p + 8) = htonl(seq);		// ARMv6 and later, x86 processors do just fine with unaligned accesses
			}
			else
			{
				// empty message with a 0 part count
				p[3] = 2;
			}
		}
		LoggerMessageAddTimestampAndThreadID(encoder);
	}
	return encoder;
}

static void LoggerMessageFinalize(CFMutableDataRef encoder)
{
	// Finalize a message by reducing the CFData size to the actual used size
	if (encoder != NULL)
	{
		uint32_t *p = (uint32_t *)CFDataGetBytePtr(encoder);
		if (p != NULL)
			CFDataSetLength(encoder, ntohl(*p) + 4);
	}
}

static void LoggerMessageAddInt32(CFMutableDataRef encoder, int32_t anInt, int key)
{
	uint8_t *p = LoggerMessagePrepareForPart(encoder, 6);
	if (p != NULL)
	{
		*p++ = (uint8_t)key;
		*p++ = (uint8_t)PART_TYPE_INT32;
		*(uint32_t *)p = htonl(anInt);		// ARMv6 and later, x86 processors do just fine with unaligned accesses
	}
}

#if __LP64__
static void LoggerMessageAddInt64(CFMutableDataRef encoder, int64_t anInt, int key)
{
	uint8_t *p = LoggerMessagePrepareForPart(encoder, 10);
	if (p != NULL)
	{
		*p++ = (uint8_t)key;
		*p++ = (uint8_t)PART_TYPE_INT64;
		uint32_t *q = (uint32_t *)p;
		*q++ = htonl((uint32_t)(anInt >> 32));	// ARMv6 and later, x86 processors do just fine with unaligned accesses
		*q = htonl((uint32_t)anInt);
	}
}
#endif

static void LoggerMessageAddCString(CFMutableDataRef data, const char *aString, int key)
{
	if (aString == NULL || *aString == 0)
		return;
	
	// convert to UTF-8
	int len = (int)strlen(aString);
	uint8_t buf[(size_t)(2 * len)];
	{
		int i, n = 0;
		for (i = 0; i < len; i++)
		{
			uint8_t c = (uint8_t)(*aString++);
			if (c < 0x80)
				buf[n++] = c;
			else {
				buf[n++] = 0xC0 | (c >> 6);
				buf[n++] = (c & 0x6F) | 0x80;
			}
		}
		if (n)
		{
			uint8_t *p = LoggerMessagePrepareForPart(data, (uint32_t)n+6);
			if (p != NULL)
			{
				*p++ = (uint8_t)key;
				*p++ = (uint8_t)PART_TYPE_STRING;
				*(uint32_t *)p = htonl(n);		// ARMv6 and later, x86 processors do just fine with unaligned accesses
				memcpy(p + 4, buf, (size_t)n);
			}
		}
	}
}

static void LoggerMessageAddString(CFMutableDataRef encoder, CFStringRef aString, int key)
{
	if (aString == NULL)
		aString = CFSTR("");

	// All strings are UTF-8 encoded
	uint32_t partSize = 0;
	CFIndex stringLength = CFStringGetLength(aString);
	CFIndex bytesLength = stringLength * 4;
	uint8_t bytes[(size_t)bytesLength + 4];
  
	if (stringLength)
	{
        CFStringGetBytes(aString, CFRangeMake(0, stringLength), kCFStringEncodingUTF8, '?', false, bytes, bytesLength, &bytesLength);
        partSize = (uint32_t)bytesLength;
	}

	uint8_t *p = LoggerMessagePrepareForPart(encoder, 6 + partSize);
	if (p != NULL)
	{
		*p++ = (uint8_t)key;
		*p++ = (uint8_t)PART_TYPE_STRING;
		*(uint32_t *)p = htonl(partSize);		// ARMv6 and later, x86 processors do just fine with unaligned accesses
		if (partSize)
			memcpy(p + 4, bytes, (size_t)partSize);
	}
}

static void LoggerMessageAddData(CFMutableDataRef encoder, CFDataRef theData, int key, int partType)
{
	if (theData != NULL)
	{
		CFIndex dataLength = CFDataGetLength(theData);
		uint8_t *p = LoggerMessagePrepareForPart(encoder, (uint32_t)dataLength + 6);
		if (p != NULL)
		{
			*p++ = (uint8_t)key;
			*p++ = (uint8_t)partType;
			*((uint32_t *)p) = htonl(dataLength);	// ARMv6 and later, x86 processors do just fine with unaligned accesses
			if (dataLength)
				memcpy(p + 4, CFDataGetBytePtr(theData), (size_t)dataLength);
		}
	}
}

static uint32_t LoggerMessageGetSeq(CFDataRef message)
{
	// Extract the sequence number from a message. When pushing messages to the queue,
	// we use this to guarantee the logging order according to the seq#
	// Since we now store the seq as first component, we only have to check whether
	// the first part is the sequence number, and extract it.
	uint8_t *p = (uint8_t *)CFDataGetBytePtr(message) + 4;
	uint16_t partCount = ntohs(*(uint16_t *)p);
	if (partCount)
	{
		if (p[2] == PART_KEY_MESSAGE_SEQ)
			return ntohl(*(uint32_t *)(p+4));		// ARMv6 and later, x86 processors do just fine with unaligned accesses
	}
	return 0;
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Private logging functions
// -----------------------------------------------------------------------------
static void	LoggerPushClientInfoToFrontOfQueue(Logger *logger)
{
	// Extract client information from the main bundle, as well as platform info,
	// and assemble it to a message that will be put in front of the queue
	// Helps desktop viewer display who's talking to it
	// Note that we must be called from the logger work thread, as we don't
	// run through the message port to transmit this message to the queue
	CFBundleRef bundle = CFBundleGetMainBundle();
	if (bundle == NULL)
		return;
	CFMutableDataRef encoder = LoggerMessageCreate(0);
	if (encoder != NULL)
	{
		LoggerMessageAddInt32(encoder, LOGMSG_TYPE_CLIENTINFO, PART_KEY_MESSAGE_TYPE);

		CFStringRef version = (CFStringRef)CFBundleGetValueForInfoDictionaryKey(bundle, kCFBundleVersionKey);
		if (version != NULL && CFGetTypeID(version) == CFStringGetTypeID())
			LoggerMessageAddString(encoder, version, PART_KEY_CLIENT_VERSION);
		CFStringRef name = (CFStringRef)CFBundleGetValueForInfoDictionaryKey(bundle, kCFBundleNameKey);
		if (name != NULL)
			LoggerMessageAddString(encoder, name, PART_KEY_CLIENT_NAME);

#if TARGET_OS_IPHONE
		if ([NSThread isMultiThreaded] || [NSThread isMainThread])
		{
			@autoreleasepool
			{
				UIDevice *device = [UIDevice currentDevice];
				LoggerMessageAddString(encoder, (__bridge CFStringRef)device.name, PART_KEY_UNIQUEID);
				LoggerMessageAddString(encoder, (__bridge CFStringRef)device.systemVersion, PART_KEY_OS_VERSION);
				LoggerMessageAddString(encoder, (__bridge CFStringRef)device.systemName, PART_KEY_OS_NAME);
				LoggerMessageAddString(encoder, (__bridge CFStringRef)device.model, PART_KEY_CLIENT_MODEL);
			}
		}
#elif TARGET_OS_MAC
		CFStringRef osName = NULL, osVersion = NULL;
		// Read the OS version without using deprecated Gestalt calls
		@autoreleasepool
		{
			@try
			{
				NSString* versionString = [[NSDictionary dictionaryWithContentsOfFile: @"/System/Library/CoreServices/SystemVersion.plist"] objectForKey: @"ProductVersion"];
				if ([versionString length])
				{
					osName = CFSTR("Mac OS X");
					osVersion = CFRetain((CFStringRef)versionString);
				}
			}
			@catch (NSException *exc)
			{
			}
		}
		if (osVersion == NULL)
		{
			// Not allowed to call into Cocoa ? use the Darwin version string
			struct utsname u;
			if (uname(&u) == 0)
			{
				osName = CFStringCreateWithCString(NULL, u.sysname, kCFStringEncodingUTF8);
				osVersion = CFStringCreateWithCString(NULL, u.release, kCFStringEncodingUTF8);
			}
			else
			{
				osName = CFSTR("Mac OS X");
				osVersion = CFSTR("");
			}
		}
		LoggerMessageAddString(encoder, osVersion, PART_KEY_OS_VERSION);
		LoggerMessageAddString(encoder, osName, PART_KEY_OS_NAME);
		CFRelease(osVersion);
		CFRelease(osName);

		char buf[64];
		size_t len;
		int ncpu = 0;
		bzero(buf, sizeof(buf));
		len = sizeof(buf)-1;
		sysctlbyname("hw.model", buf, &len, NULL, 0);
		len = sizeof(ncpu);
		sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0);
		sprintf(buf+strlen(buf), " - %d * ", ncpu);
		len = sizeof(buf)-strlen(buf)-1;
		sysctlbyname("hw.machine", buf+strlen(buf), &len, NULL, 0);
		
		CFStringRef s = CFStringCreateWithCString(NULL, buf, kCFStringEncodingASCII);
		LoggerMessageAddString(encoder, s, PART_KEY_CLIENT_MODEL);
		CFRelease(s);
#endif
		LoggerMessageFinalize(encoder);

		pthread_mutex_lock(&logger->logQueueMutex);
		CFArrayInsertValueAtIndex(logger->logQueue, logger->incompleteSendOfFirstItem ? 1 : 0, encoder);
		pthread_mutex_unlock(&logger->logQueueMutex);

		CFRelease(encoder);
	}
}

static void LoggerPushMessageToQueue(Logger *logger, CFDataRef message)
{
	// Add the message to the log queue and signal the runLoop source that will trigger
	// a send on the worker thread.
	pthread_mutex_lock(&logger->logQueueMutex);
	CFIndex idx = CFArrayGetCount(logger->logQueue);
	if (idx)
	{
		// to prevent out-of-order messages (as much as possible), we try to transmit messages in the
		// order their sequence number was generated. Since the seq is generated first-thing,
		// we can provide fine-grained ordering that gives a reasonable idea of the order
		// the logging calls were made (useful for precise information about multithreading code)
		uint32_t lastSeq, seq = LoggerMessageGetSeq(message);
		do {
			lastSeq = LoggerMessageGetSeq(CFArrayGetValueAtIndex(logger->logQueue, idx-1));
		} while (lastSeq > seq && --idx > 0);
	}
	if (idx >= 0)
		CFArrayInsertValueAtIndex(logger->logQueue, idx, message);
	else
		CFArrayAppendValue(logger->logQueue, message);
	
	if (logger->messagePushedSource != NULL)
	{
		// One case where the pushed source may be NULL is if the client code
		// immediately starts logging without initializing the logger first.
		// In this case, the worker thread has not completed startup, so we don't need
		// to fire the runLoop source
		CFRunLoopSourceSignal(logger->messagePushedSource);
	}
	else if (logger->workerThread == NULL && (logger->options & kLoggerOption_LogToConsole) && !(logger->options & kLoggerOption_CaptureSystemConsole))
	{
		// In this case, a failure creating the message runLoop source forces us
		// to always log to console
		while (CFArrayGetCount(logger->logQueue))
		{
			LoggerLogToConsole(CFArrayGetValueAtIndex(logger->logQueue, 0));
			CFArrayRemoveValueAtIndex(logger->logQueue, 0);
		}
		pthread_cond_broadcast(&logger->logQueueEmpty);		// in case other threads are waiting for a flush
	}
	pthread_mutex_unlock(&logger->logQueueMutex);
}

static void LogMessageRawTo_internal(Logger *logger,
								  const char *filename,
								  int lineNumber,
								  const char *functionName,
								  NSString *domain,
								  int level,
								  NSString *message)
{
	// Variant of the LogMessage function that doesn't perform any variable arguments formatting
	logger = LoggerStart(logger);	// start if needed
    if (logger != NULL)
	{
        int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
        LOGGERDBG2(CFSTR("%ld LogMessage"), seq);

        CFMutableDataRef encoder = LoggerMessageCreate(seq);
        if (encoder != NULL)
        {
            LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE);
            if (domain != nil && [domain length])
                LoggerMessageAddString(encoder, (__bridge CFStringRef)domain, PART_KEY_TAG);
            if (level)
                LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL);
            if (filename != NULL)
                LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME);
            if (lineNumber)
                LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER);
            if (functionName != NULL)
                LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME);
            if (message != nil)
                LoggerMessageAddString(encoder, (__bridge CFStringRef)message, PART_KEY_MESSAGE);
			else
				LoggerMessageAddString(encoder, CFSTR(""), PART_KEY_MESSAGE);

			LoggerMessageFinalize(encoder);
            LoggerPushMessageToQueue(logger, encoder);
            CFRelease(encoder);
        }
        else
        {
            LOGGERDBG2(CFSTR("-> failed creating encoder"));
        }
    }
}

static void LogMessageTo_internal(Logger *logger,
								  const char *filename,
								  int lineNumber,
								  const char *functionName,
								  NSString *domain,
								  int level,
								  NSString *format,
								  va_list args)
{
	logger = LoggerStart(logger);	// start if needed
    if (logger != NULL)
	{
        int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
        LOGGERDBG2(CFSTR("%ld LogMessage"), seq);

        CFMutableDataRef encoder = LoggerMessageCreate(seq);
        if (encoder != NULL)
        {
            LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE);
            if (domain != nil && [domain length])
                LoggerMessageAddString(encoder, (__bridge CFStringRef)domain, PART_KEY_TAG);
            if (level)
                LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL);
            if (filename != NULL)
                LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME);
            if (lineNumber)
                LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER);
            if (functionName != NULL)
                LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME);

            NSString *msgString = [[NSString alloc] initWithFormat:format arguments:args];
            if (msgString != nil)
            {
                LoggerMessageAddString(encoder, (__bridge CFStringRef)msgString, PART_KEY_MESSAGE);
//                [msgString release];
            }

			LoggerMessageFinalize(encoder);
            LoggerPushMessageToQueue(logger, encoder);
            CFRelease(encoder);
        }
        else
        {
            LOGGERDBG2(CFSTR("-> failed creating encoder"));
        }
    }
}

static void LogImageTo_internal(Logger *logger,
								const char *filename,
								int lineNumber,
								const char *functionName,
								NSString *domain,
								int level,
								int width,
								int height,
								NSData *data)
{
	logger = LoggerStart(logger);		// start if needed
	if (logger != NULL)
	{
		int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
		LOGGERDBG2(CFSTR("%ld LogImage"), seq);

		CFMutableDataRef encoder = LoggerMessageCreate(seq);
		if (encoder != NULL)
		{
			LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE);
			if (domain != nil && [domain length])
				LoggerMessageAddString(encoder, (__bridge CFStringRef)domain, PART_KEY_TAG);
			if (level)
				LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL);
			if (width && height)
			{
				LoggerMessageAddInt32(encoder, width, PART_KEY_IMAGE_WIDTH);
				LoggerMessageAddInt32(encoder, height, PART_KEY_IMAGE_HEIGHT);
			}
			if (filename != NULL)
				LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME);
			if (lineNumber)
				LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER);
			if (functionName != NULL)
				LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME);
			LoggerMessageAddData(encoder, (__bridge CFDataRef)data, PART_KEY_MESSAGE, PART_TYPE_IMAGE);

			LoggerMessageFinalize(encoder);
			LoggerPushMessageToQueue(logger, encoder);
			CFRelease(encoder);
		}
		else
		{
			LOGGERDBG2(CFSTR("-> failed creating encoder"));
		}
	}
}

static void LogDataTo_internal(Logger *logger,
							   const char *filename,
							   int lineNumber,
							   const char *functionName,
							   NSString *domain,
							   int level, NSData *data)
{
	logger = LoggerStart(logger);		// start if needed
    if (logger != NULL)
    {
        int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
        LOGGERDBG2(CFSTR("%ld LogData"), seq);

        CFMutableDataRef encoder = LoggerMessageCreate(seq);
        if (encoder != NULL)
        {
            LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE);
            if (domain != nil && [domain length])
                LoggerMessageAddString(encoder, (__bridge CFStringRef)domain, PART_KEY_TAG);
            if (level)
                LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL);
            if (filename != NULL)
                LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME);
            if (lineNumber)
                LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER);
            if (functionName != NULL)
                LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME);
            LoggerMessageAddData(encoder, (__bridge CFDataRef)data, PART_KEY_MESSAGE, PART_TYPE_BINARY);

			LoggerMessageFinalize(encoder);
            LoggerPushMessageToQueue(logger, encoder);
            CFRelease(encoder);
        }
        else
        {
            LOGGERDBG2(CFSTR("-> failed creating encoder"));
        }
    }
}

static void LogStartBlockTo_internal(Logger *logger, NSString *format, va_list args)
{
	logger = LoggerStart(logger);		// start if needed
	if (logger)
	{
		int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
		LOGGERDBG2(CFSTR("%ld LogStartBlock"), seq);

		CFMutableDataRef encoder = LoggerMessageCreate(seq);
		if (encoder != NULL)
		{
			LoggerMessageAddInt32(encoder, LOGMSG_TYPE_BLOCKSTART, PART_KEY_MESSAGE_TYPE);
			if (format != nil)
			{
				CFStringRef msgString = CFStringCreateWithFormatAndArguments(NULL, NULL, (CFStringRef)format, args);
				if (msgString != NULL)
				{
					LoggerMessageAddString(encoder, msgString, PART_KEY_MESSAGE);
					CFRelease(msgString);
				}
			}
		
			LoggerMessageFinalize(encoder);
			LoggerPushMessageToQueue(logger, encoder);
			CFRelease(encoder);
		}
	}
}

// -----------------------------------------------------------------------------
#pragma mark -
#pragma mark Public logging functions
// -----------------------------------------------------------------------------
void LogMessageRaw(NSString *message)
{
	LogMessageRawTo_internal(NULL, NULL, 0, NULL, nil, 0, message);
}

void LogMessageRawF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message)
{
	LogMessageRawTo_internal(NULL, filename, lineNumber, functionName, domain, level, message);
}

void LogMessageRawToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message)
{
	LogMessageRawTo_internal(logger, filename, lineNumber, functionName, domain, level, message);
}

void LogMessageCompat(NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogMessageTo_internal(NULL, NULL, 0, NULL, nil, 0, format, args);
	va_end(args);
}

void LogMessageTo(Logger *logger, NSString *domain, int level, NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogMessageTo_internal(logger, NULL, 0, NULL, domain, level, format, args);
	va_end(args);
}

void LogMessageToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogMessageTo_internal(logger, filename, lineNumber, functionName, domain, level, format, args);
	va_end(args);
}

void LogMessageTo_va(Logger *logger, NSString *domain, int level, NSString *format, va_list args)
{
	LogMessageTo_internal(logger, NULL, 0, NULL, domain, level, format, args);
}

void LogMessageToF_va(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args)
{
	LogMessageTo_internal(logger, filename, lineNumber, functionName, domain, level, format, args);
}

void LogMessage(NSString *domain, int level, NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogMessageTo_internal(NULL, NULL, 0, NULL, domain, level, format, args);
	va_end(args);
}

void LogMessageF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogMessageTo_internal(NULL, filename, lineNumber, functionName, domain, level, format, args);
	va_end(args);
}

void LogMessage_va(NSString *domain, int level, NSString *format, va_list args)
{
	LogMessageTo_internal(NULL, NULL, 0, NULL, domain, level, format, args);
}

void LogMessageF_va(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args)
{
	LogMessageTo_internal(NULL, filename, lineNumber, functionName, domain, level, format, args);
}

void LogData(NSString *domain, int level, NSData *data)
{
	LogDataTo_internal(NULL, NULL, 0, NULL, domain, level, data);
}

void LogDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data)
{
	LogDataTo_internal(NULL, filename, lineNumber, functionName, domain, level, data);
}

void LogDataTo(Logger *logger, NSString *domain, int level, NSData *data)
{
	LogDataTo_internal(logger, NULL, 0, NULL, domain, level, data);
}

void LogDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data)
{
	LogDataTo_internal(logger, filename, lineNumber, functionName, domain, level, data);
}

void LogImageData(NSString *domain, int level, int width, int height, NSData *data)
{
	LogImageTo_internal(NULL, NULL, 0, NULL, domain, level, width, height, data);
}

void LogImageDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data)
{
	LogImageTo_internal(NULL, filename, lineNumber, functionName, domain, level, width, height, data);
}

void LogImageDataTo(Logger *logger, NSString *domain, int level, int width, int height, NSData *data)
{
	LogImageTo_internal(logger, NULL, 0, NULL, domain, level, width, height, data);
}

void LogImageDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data)
{
	LogImageTo_internal(logger, filename, lineNumber, functionName, domain, level, width, height, data);
}

void LogStartBlock(NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogStartBlockTo_internal(NULL, format, args);
	va_end(args);
}

void LogStartBlockTo(Logger *logger, NSString *format, ...)
{
	va_list args;
	va_start(args, format);
	LogStartBlockTo_internal(logger, format, args);
	va_end(args);
}

void LogEndBlockTo(Logger *logger)
{
	logger = LoggerStart(logger);
    if (logger)
    {
        if (logger->options & kLoggerOption_LogToConsole)
            return;

        int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
        LOGGERDBG2(CFSTR("%ld LogEndBlock"), seq);

        CFMutableDataRef encoder = LoggerMessageCreate(seq);
        if (encoder != NULL)
        {
            LoggerMessageAddInt32(encoder, LOGMSG_TYPE_BLOCKEND, PART_KEY_MESSAGE_TYPE);
			LoggerMessageFinalize(encoder);
            LoggerPushMessageToQueue(logger, encoder);
            CFRelease(encoder);
        }
        else
        {
            LOGGERDBG2(CFSTR("-> failed creating encoder"));
        }
    }
}

void LogEndBlock(void)
{
	LogEndBlockTo(NULL);
}

void LogMarkerTo(Logger *logger, NSString *text)
{
	logger = LoggerStart(logger);		// start if needed
	if (logger != NULL)
	{
		int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq);
		LOGGERDBG2(CFSTR("%ld LogMarker"), seq);

		CFMutableDataRef encoder = LoggerMessageCreate(seq);
		if (encoder != NULL)
		{
			LoggerMessageAddInt32(encoder, LOGMSG_TYPE_MARK, PART_KEY_MESSAGE_TYPE);
			if (text == nil)
			{
				CFDateFormatterRef df = CFDateFormatterCreate(NULL, NULL, kCFDateFormatterShortStyle, kCFDateFormatterMediumStyle);
				CFStringRef str = CFDateFormatterCreateStringWithAbsoluteTime(NULL, df, CFAbsoluteTimeGetCurrent());
				CFRelease(df);
				if (str != NULL)
				{
					LoggerMessageAddString(encoder, str, PART_KEY_MESSAGE);
					CFRelease(str);
				}
			}
			else
			{
				LoggerMessageAddString(encoder, (__bridge CFStringRef)text, PART_KEY_MESSAGE);
			}
			LoggerMessageFinalize(encoder);
			LoggerPushMessageToQueue(logger, encoder);
			CFRelease(encoder);
		}
		else
		{
			LOGGERDBG2(CFSTR("-> failed creating encoder"));
		}
	}
}

void LogMarker(NSString *text)
{
	LogMarkerTo(NULL, text);
}
