// Software License Agreement (BSD License)
//
// Copyright (c) 2010-2024, Deusty, LLC
// All rights reserved.
//
// Redistribution and use of this software 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.
//
// * Neither the name of Deusty nor the names of its contributors may be used
//   to endorse or promote products derived from this software without specific
//   prior written permission of Deusty, LLC.

#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

#import <sys/uio.h>

#import "DDTTYLogger.h"

// We probably shouldn't be using DDLog() statements within the DDLog implementation.
// But we still want to leave our log statements for any future debugging,
// and to allow other developers to trace the implementation (which is a great learning tool).
//
// So we use primitive logging macros around NSLog.
// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog.

#ifndef DD_NSLOG_LEVEL
    #define DD_NSLOG_LEVEL 2
#endif

#define NSLogError(frmt, ...)    do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogWarn(frmt, ...)     do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogInfo(frmt, ...)     do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogDebug(frmt, ...)    do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogVerbose(frmt, ...)  do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0)

// Xcode does NOT natively support colors in the Xcode debugging console.
// You'll need to install the XcodeColors plugin to see colors in the Xcode console.
// https://github.com/robbiehanson/XcodeColors
//
// The following is documentation from the XcodeColors project:
//
//
// How to apply color formatting to your log statements:
//
// To set the foreground color:
// Insert the ESCAPE_SEQ into your string, followed by "fg124,12,255;" where r=124, g=12, b=255.
//
// To set the background color:
// Insert the ESCAPE_SEQ into your string, followed by "bg12,24,36;" where r=12, g=24, b=36.
//
// To reset the foreground color (to default value):
// Insert the ESCAPE_SEQ into your string, followed by "fg;"
//
// To reset the background color (to default value):
// Insert the ESCAPE_SEQ into your string, followed by "bg;"
//
// To reset the foreground and background color (to default values) in one operation:
// Insert the ESCAPE_SEQ into your string, followed by ";"

#define XCODE_COLORS_ESCAPE_SEQ "\033["

#define XCODE_COLORS_RESET_FG   XCODE_COLORS_ESCAPE_SEQ "fg;" // Clear any foreground color
#define XCODE_COLORS_RESET_BG   XCODE_COLORS_ESCAPE_SEQ "bg;" // Clear any background color
#define XCODE_COLORS_RESET      XCODE_COLORS_ESCAPE_SEQ ";"  // Clear any foreground or background color

// If running in a shell, not all RGB colors will be supported.
// In this case we automatically map to the closest available color.
// In order to provide this mapping, we have a hard-coded set of the standard RGB values available in the shell.
// However, not every shell is the same, and Apple likes to think different even when it comes to shell colors.
//
// Map to standard Terminal.app colors (1), or
// map to standard xterm colors (0).

#define MAP_TO_TERMINAL_APP_COLORS 1

typedef struct {
    uint8_t r;
    uint8_t g;
    uint8_t b;
} DDRGBColor;

@interface DDTTYLoggerColorProfile : NSObject {
@public
    DDLogFlag mask;
    NSInteger context;

    DDRGBColor fg;
    DDRGBColor bg;

    NSUInteger fgCodeIndex;
    NSString *fgCodeRaw;

    NSUInteger bgCodeIndex;
    NSString *bgCodeRaw;

    char fgCode[24];
    size_t fgCodeLen;

    char bgCode[24];
    size_t bgCodeLen;

    char resetCode[8];
    size_t resetCodeLen;
}

- (nullable instancetype)initWithForegroundColor:(nullable DDColor *)fgColor backgroundColor:(nullable DDColor *)bgColor flag:(DDLogFlag)mask context:(NSInteger)ctxt;

@end

@interface DDTTYLogger () {
    NSString *_appName;
    char *_app;
    size_t _appLen;

    NSString *_processID;
    char *_pid;
    size_t _pidLen;

    BOOL _colorsEnabled;
    NSMutableArray *_colorProfilesArray;
    NSMutableDictionary *_colorProfilesDict;
}

@end

#pragma mark -

@implementation DDTTYLogger

static BOOL isaColorTTY;
static BOOL isaColor256TTY;
static BOOL isaXcodeColorTTY;

static NSArray *codesFg = nil;
static NSArray *codesBg = nil;
static NSArray *colors   = nil;

static DDTTYLogger *sharedInstance;

/**
 * Initializes the colors array, as well as the `codesFg` and `codesBg` arrays, for 16 color mode.
 *
 * This method is used when the application is running from within a shell that only supports 16 color mode.
 * This method is not invoked if the application is running within Xcode, or via normal UI app launch.
 **/
+ (void)initializeColors16 {
    if (codesFg || codesBg || colors) {
        return;
    }

    __auto_type mColors = [NSMutableArray arrayWithCapacity:16];

    // In a standard shell only 16 colors are supported.
    //
    // More information about ansi escape codes can be found online.
    // http://en.wikipedia.org/wiki/ANSI_escape_code
    codesFg = @[
        @"30m",  // normal - black
        @"31m",  // normal - red
        @"32m",  // normal - green
        @"33m",  // normal - yellow
        @"34m",  // normal - blue
        @"35m",  // normal - magenta
        @"36m",  // normal - cyan
        @"37m",  // normal - gray
        @"1;30m",  // bright - darkgray
        @"1;31m",  // bright - red
        @"1;32m",  // bright - green
        @"1;33m",  // bright - yellow
        @"1;34m",  // bright - blue
        @"1;35m",  // bright - magenta
        @"1;36m",  // bright - cyan
        @"1;37m",  // bright - white
    ];

    codesBg = @[
        @"40m",  // normal - black
        @"41m",  // normal - red
        @"42m",  // normal - green
        @"43m",  // normal - yellow
        @"44m",  // normal - blue
        @"45m",  // normal - magenta
        @"46m",  // normal - cyan
        @"47m",  // normal - gray
        @"1;40m",  // bright - darkgray
        @"1;41m",  // bright - red
        @"1;42m",  // bright - green
        @"1;43m",  // bright - yellow
        @"1;44m",  // bright - blue
        @"1;45m",  // bright - magenta
        @"1;46m",  // bright - cyan
        @"1;47m",  // bright - white
    ];

#if MAP_TO_TERMINAL_APP_COLORS

    // Standard Terminal.app colors:
    //
    // These are the default colors used by Apple's Terminal.app.
    const DDRGBColor rgbColors[] = {
        {  0,   0,   0}, // normal - black
        {194,  54,  33}, // normal - red
        { 37, 188,  36}, // normal - green
        {173, 173,  39}, // normal - yellow
        { 73,  46, 225}, // normal - blue
        {211,  56, 211}, // normal - magenta
        { 51, 187, 200}, // normal - cyan
        {203, 204, 205}, // normal - gray
        {129, 131, 131}, // bright - darkgray
        {252,  57,  31}, // bright - red
        { 49, 231,  34}, // bright - green
        {234, 236,  35}, // bright - yellow
        { 88,  51, 255}, // bright - blue
        {249,  53, 248}, // bright - magenta
        { 20, 240, 240}, // bright - cyan
        {233, 235, 235}, // bright - white
    };

#else /* if MAP_TO_TERMINAL_APP_COLORS */

    // Standard xterm colors:
    //
    // These are the default colors used by most xterm shells.
    const DDRGBColor rgbColors[] = {
        {  0,   0,   0}, // normal - black
        {205,   0,   0}, // normal - red
        {  0, 205,   0}, // normal - green
        {205, 205,   0}, // normal - yellow
        {  0,   0, 238}, // normal - blue
        {205,   0, 205}, // normal - magenta
        {  0, 205, 205}, // normal - cyan
        {229, 229, 229}, // normal - gray
        {127, 127, 127}, // bright - darkgray
        {255,   0,   0}, // bright - red
        {  0, 255,   0}, // bright - green
        {255, 255,   0}, // bright - yellow
        { 92,  92, 255}, // bright - blue
        {255,   0, 255}, // bright - magenta
        {  0, 255, 255}, // bright - cyan
        {255, 255, 255}, // bright - white
    };
#endif /* if MAP_TO_TERMINAL_APP_COLORS */

    for (size_t i = 0; i < sizeof(rgbColors) / sizeof(rgbColors[0]); ++i) {
        [mColors addObject:DDMakeColor(rgbColors[i].r, rgbColors[i].g, rgbColors[i].b)];
    }
    colors = [mColors copy];

    NSAssert([codesFg count] == [codesBg count], @"Invalid colors/codes array(s)");
    NSAssert([codesFg count] == [colors count],   @"Invalid colors/codes array(s)");
}

/**
 * Initializes the colors array, as well as the `codesFg` and `codesBg` arrays, for 256 color mode.
 *
 * This method is used when the application is running from within a shell that supports 256 color mode.
 * This method is not invoked if the application is running within Xcode, or via normal UI app launch.
 **/
+ (void)initializeColors256 {
    if (codesFg || codesBg || colors) {
        return;
    }

    __auto_type mCodesFg = [NSMutableArray arrayWithCapacity:(256 - 16)];
    __auto_type mCodesBg = [NSMutableArray arrayWithCapacity:(256 - 16)];
    __auto_type mColors  = [NSMutableArray arrayWithCapacity:(256 - 16)];

#if MAP_TO_TERMINAL_APP_COLORS

    // Standard Terminal.app colors:
    //
    // These are the colors the Terminal.app uses in xterm-256color mode.
    // In this mode, the terminal supports 256 different colors, specified by 256 color codes.
    //
    // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode.
    // These are actually configurable, and thus we ignore them for the purposes of mapping,
    // as we can't rely on them being constant. They are largely duplicated anyway.
    //
    // The next 216 color codes are designed to run the spectrum, with several shades of every color.
    // While the color codes are standardized, the actual RGB values for each color code is not.
    // Apple's Terminal.app uses different RGB values from that of a standard xterm.
    // Apple's choices in colors are designed to be a little nicer on the eyes.
    //
    // The last 24 color codes represent a grayscale.
    //
    // Unfortunately, unlike the standard xterm color chart,
    // Apple's RGB values cannot be calculated using a simple formula (at least not that I know of).
    // Also, I don't know of any ways to programmatically query the shell for the RGB values.
    // So this big giant color chart had to be made by hand.
    //
    // More information about ansi escape codes can be found online.
    // http://en.wikipedia.org/wiki/ANSI_escape_code

    // Colors
    const DDRGBColor rgbColors[] = {
        { 47,  49,  49},
        { 60,  42, 144},
        { 66,  44, 183},
        { 73,  46, 222},
        { 81,  50, 253},
        { 88,  51, 255},

        { 42, 128,  37},
        { 42, 127, 128},
        { 44, 126, 169},
        { 56, 125, 209},
        { 59, 124, 245},
        { 66, 123, 255},

        { 51, 163,  41},
        { 39, 162, 121},
        { 42, 161, 162},
        { 53, 160, 202},
        { 45, 159, 240},
        { 58, 158, 255},

        { 31, 196,  37},
        { 48, 196, 115},
        { 39, 195, 155},
        { 49, 195, 195},
        { 32, 194, 235},
        { 53, 193, 255},

        { 50, 229,  35},
        { 40, 229, 109},
        { 27, 229, 149},
        { 49, 228, 189},
        { 33, 228, 228},
        { 53, 227, 255},

        { 27, 254,  30},
        { 30, 254, 103},
        { 45, 254, 143},
        { 38, 253, 182},
        { 38, 253, 222},
        { 42, 253, 252},

        {140,  48,  40},
        {136,  51, 136},
        {135,  52, 177},
        {134,  52, 217},
        {135,  56, 248},
        {134,  53, 255},

        {125, 125,  38},
        {124, 125, 125},
        {122, 124, 166},
        {123, 124, 207},
        {123, 122, 247},
        {124, 121, 255},

        {119, 160,  35},
        {117, 160, 120},
        {117, 160, 160},
        {115, 159, 201},
        {116, 158, 240},
        {117, 157, 255},

        {113, 195,  39},
        {110, 194, 114},
        {111, 194, 154},
        {108, 194, 194},
        {109, 193, 234},
        {108, 192, 255},

        {105, 228,  30},
        {103, 228, 109},
        {105, 228, 148},
        {100, 227, 188},
        { 99, 227, 227},
        { 99, 226, 253},

        { 92, 253,  34},
        { 96, 253, 103},
        { 97, 253, 142},
        { 88, 253, 182},
        { 93, 253, 221},
        { 88, 254, 251},

        {177,  53,  34},
        {174,  54, 131},
        {172,  55, 172},
        {171,  57, 213},
        {170,  55, 249},
        {170,  57, 255},

        {165, 123,  37},
        {163, 123, 123},
        {162, 123, 164},
        {161, 122, 205},
        {161, 121, 241},
        {161, 121, 255},

        {158, 159,  33},
        {157, 158, 118},
        {157, 158, 159},
        {155, 157, 199},
        {155, 157, 239},
        {154, 156, 255},

        {152, 193,  40},
        {151, 193, 113},
        {150, 193, 153},
        {150, 192, 193},
        {148, 192, 232},
        {149, 191, 253},

        {146, 227,  28},
        {144, 227, 108},
        {144, 227, 147},
        {144, 227, 187},
        {142, 226, 227},
        {142, 225, 252},

        {138, 253,  36},
        {137, 253, 102},
        {136, 253, 141},
        {138, 254, 181},
        {135, 255, 220},
        {133, 255, 250},

        {214,  57,  30},
        {211,  59, 126},
        {209,  57, 168},
        {208,  55, 208},
        {207,  58, 247},
        {206,  61, 255},

        {204, 121,  32},
        {202, 121, 121},
        {201, 121, 161},
        {200, 120, 202},
        {200, 120, 241},
        {198, 119, 255},

        {198, 157,  37},
        {196, 157, 116},
        {195, 156, 157},
        {195, 156, 197},
        {194, 155, 236},
        {193, 155, 255},

        {191, 192,  36},
        {190, 191, 112},
        {189, 191, 152},
        {189, 191, 191},
        {188, 190, 230},
        {187, 190, 253},

        {185, 226,  28},
        {184, 226, 106},
        {183, 225, 146},
        {183, 225, 186},
        {182, 225, 225},
        {181, 224, 252},

        {178, 255,  35},
        {178, 255, 101},
        {177, 254, 141},
        {176, 254, 180},
        {176, 254, 220},
        {175, 253, 249},

        {247,  56,  30},
        {245,  57, 122},
        {243,  59, 163},
        {244,  60, 204},
        {242,  59, 241},
        {240,  55, 255},

        {241, 119,  36},
        {240, 120, 118},
        {238, 119, 158},
        {237, 119, 199},
        {237, 118, 238},
        {236, 118, 255},

        {235, 154,  36},
        {235, 154, 114},
        {234, 154, 154},
        {232, 154, 194},
        {232, 153, 234},
        {232, 153, 255},

        {230, 190,  30},
        {229, 189, 110},
        {228, 189, 150},
        {227, 189, 190},
        {227, 189, 229},
        {226, 188, 255},

        {224, 224,  35},
        {223, 224, 105},
        {222, 224, 144},
        {222, 223, 184},
        {222, 223, 224},
        {220, 223, 253},

        {217, 253,  28},
        {217, 253,  99},
        {216, 252, 139},
        {216, 252, 179},
        {215, 252, 218},
        {215, 251, 250},

        {255,  61,  30},
        {255,  60, 118},
        {255,  58, 159},
        {255,  56, 199},
        {255,  55, 238},
        {255,  59, 255},

        {255, 117,  29},
        {255, 117, 115},
        {255, 117, 155},
        {255, 117, 195},
        {255, 116, 235},
        {254, 116, 255},

        {255, 152,  27},
        {255, 152, 111},
        {254, 152, 152},
        {255, 152, 192},
        {254, 151, 231},
        {253, 151, 253},

        {255, 187,  33},
        {253, 187, 107},
        {252, 187, 148},
        {253, 187, 187},
        {254, 187, 227},
        {252, 186, 252},

        {252, 222,  34},
        {251, 222, 103},
        {251, 222, 143},
        {250, 222, 182},
        {251, 221, 222},
        {252, 221, 252},

        {251, 252,  15},
        {251, 252,  97},
        {249, 252, 137},
        {247, 252, 177},
        {247, 253, 217},
        {254, 255, 255},

        // Grayscale

        { 52,  53,  53},
        { 57,  58,  59},
        { 66,  67,  67},
        { 75,  76,  76},
        { 83,  85,  85},
        { 92,  93,  94},

        {101, 102, 102},
        {109, 111, 111},
        {118, 119, 119},
        {126, 127, 128},
        {134, 136, 136},
        {143, 144, 145},

        {151, 152, 153},
        {159, 161, 161},
        {167, 169, 169},
        {176, 177, 177},
        {184, 185, 186},
        {192, 193, 194},

        {200, 201, 202},
        {208, 209, 210},
        {216, 218, 218},
        {224, 226, 226},
        {232, 234, 234},
        {240, 242, 242},
    };

    for (size_t i = 0; i < sizeof(rgbColors) / sizeof(rgbColors[0]); ++i) {
        [mColors addObject:DDMakeColor(rgbColors[i].r, rgbColors[i].g, rgbColors[i].b)];
    }

    // Color codes
    int index = 16;
    while (index < 256) {
        [mCodesFg addObject:[NSString stringWithFormat:@"38;5;%dm", index]];
        [mCodesBg addObject:[NSString stringWithFormat:@"48;5;%dm", index]];

        index++;
    }

#else /* if MAP_TO_TERMINAL_APP_COLORS */

    // Standard xterm colors:
    //
    // These are the colors xterm shells use in xterm-256color mode.
    // In this mode, the shell supports 256 different colors, specified by 256 color codes.
    //
    // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode.
    // These are generally configurable, and thus we ignore them for the purposes of mapping,
    // as we can't rely on them being constant. They are largely duplicated anyway.
    //
    // The next 216 color codes are designed to run the spectrum, with several shades of every color.
    // The last 24 color codes represent a grayscale.
    //
    // While the color codes are standardized, the actual RGB values for each color code is not.
    // However most standard xterms follow a well known color chart,
    // which can easily be calculated using the simple formula below.
    //
    // More information about ansi escape codes can be found online.
    // http://en.wikipedia.org/wiki/ANSI_escape_code

    int index = 16;

    int r; // red
    int g; // green
    int b; // blue

    int ri; // r increment
    int gi; // g increment
    int bi; // b increment

    // Calculate xterm colors (using standard algorithm)

    int r = 0;
    int g = 0;
    int b = 0;

    for (ri = 0; ri < 6; ri++) {
        r = (ri == 0) ? 0 : 95 + (40 * (ri - 1));

        for (gi = 0; gi < 6; gi++) {
            g = (gi == 0) ? 0 : 95 + (40 * (gi - 1));

            for (bi = 0; bi < 6; bi++) {
                b = (bi == 0) ? 0 : 95 + (40 * (bi - 1));

                [mCodesFg addObject:[NSString stringWithFormat:@"38;5;%dm", index]];
                [mCodesBg addObject:[NSString stringWithFormat:@"48;5;%dm", index]];
                [mColors  addObject:DDMakeColor(r, g, b)];

                index++;
            }
        }
    }

    // Calculate xterm grayscale (using standard algorithm)

    r = 8;
    g = 8;
    b = 8;

    while (index < 256) {
        [mCodesFg addObject:[NSString stringWithFormat:@"38;5;%dm", index]];
        [mCodesBg addObject:[NSString stringWithFormat:@"48;5;%dm", index]];
        [mColor s addObject:DDMakeColor(r, g, b)];

        r += 10;
        g += 10;
        b += 10;

        index++;
    }

#endif /* if MAP_TO_TERMINAL_APP_COLORS */

    codesFg = [mCodesFg copy];
    codesBg = [mCodesBg copy];
    colors  = [mColors  copy];

    NSAssert([codesFg count] == [codesBg count], @"Invalid colors/codes array(s)");
    NSAssert([codesFg count] == [colors count],   @"Invalid colors/codes array(s)");
}

+ (void)getRed:(CGFloat *)rPtr green:(CGFloat *)gPtr blue:(CGFloat *)bPtr fromColor:(DDColor *)color {
#if TARGET_OS_IPHONE

    // iOS
    __auto_type done = NO;

    if ([color respondsToSelector:@selector(getRed:green:blue:alpha:)]) {
        done = [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL];
    }

    if (!done) {
        // The method getRed:green:blue:alpha: was only available starting iOS 5.
        // So in iOS 4 and earlier, we have to jump through hoops.

        __auto_type rgbColorSpace = CGColorSpaceCreateDeviceRGB();

        unsigned char pixel[4];
        __auto_type context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, rgbColorSpace, (CGBitmapInfo)(kCGBitmapAlphaInfoMask & kCGImageAlphaNoneSkipLast));

        CGContextSetFillColorWithColor(context, [color CGColor]);
        CGContextFillRect(context, CGRectMake(0, 0, 1, 1));

        if (rPtr) {
            *rPtr = pixel[0] / 255.0;
        }
        if (gPtr) {
            *gPtr = pixel[1] / 255.0;
        }
        if (bPtr) {
            *bPtr = pixel[2] / 255.0;
        }

        CGContextRelease(context);
        CGColorSpaceRelease(rgbColorSpace);
    }

#elif defined(DD_CLI) || !__has_include(<AppKit/NSColor.h>)

    // OS X without AppKit
    [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL];

#else /* if TARGET_OS_IPHONE */

    // OS X with AppKit
    NSColor *safeColor;
    if (@available(macOS 10.14,*)) {
        safeColor = [color colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        safeColor = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
#pragma clang diagnostic pop
    }

    [safeColor getRed:rPtr green:gPtr blue:bPtr alpha:NULL];

#endif /* if TARGET_OS_IPHONE */
}

/**
 * Maps the given color to the closest available color supported by the shell.
 * The shell may support 256 colors, or only 16.
 *
 * This method loops through the known supported color set, and calculates the closest color.
 * The array index of that color, within the colors array, is then returned.
 * This array index may also be used as the index within the `codesFg` and `codesBg` arrays.
 **/
+ (NSUInteger)codeIndexForColor:(DDColor *)inColor {
    CGFloat inR, inG, inB;

    [self getRed:&inR green:&inG blue:&inB fromColor:inColor];

    NSUInteger bestIndex = 0;
    CGFloat lowestDistance = 100.0;

    NSUInteger i = 0;

    for (DDColor *color in colors) {
        // Calculate Euclidean distance (lower value means closer to given color)

        CGFloat r, g, b;
        [self getRed:&r green:&g blue:&b fromColor:color];

#if CGFLOAT_IS_DOUBLE
        __auto_type distance = sqrt(pow(r - inR, 2.0) + pow(g - inG, 2.0) + pow(b - inB, 2.0));
#else
        __auto_type distance = sqrtf(powf(r - inR, 2.0f) + powf(g - inG, 2.0f) + powf(b - inB, 2.0f));
#endif

        NSLogVerbose(@"DDTTYLogger: %3lu : %.3f,%.3f,%.3f & %.3f,%.3f,%.3f = %.6f",
                     (unsigned long)i, (double)inR, (double)inG, (double)inB, (double)r, (double)g, (double)b, (double)distance);

        if (distance < lowestDistance) {
            bestIndex = i;
            lowestDistance = distance;

            NSLogVerbose(@"DDTTYLogger: New best index = %lu", (unsigned long)bestIndex);
        }

        i++;
    }

    return bestIndex;
}

+ (instancetype)sharedInstance {
    static dispatch_once_t DDTTYLoggerOnceToken;

    dispatch_once(&DDTTYLoggerOnceToken, ^{
        // Xcode does NOT natively support colors in the Xcode debugging console.
        // You'll need to install the XcodeColors plugin to see colors in the Xcode console.
        //
        // PS - Please read the header file before diving into the source code.

        __auto_type xcodeColors = getenv("XcodeColors");
        __auto_type term = getenv("TERM");

        if (xcodeColors && (strcmp(xcodeColors, "YES") == 0)) {
            isaXcodeColorTTY = YES;
        } else if (term) {
            if (strcasestr(term, "color") != NULL) {
                isaColorTTY = YES;
                isaColor256TTY = (strcasestr(term, "256") != NULL);

                if (isaColor256TTY) {
                    [self initializeColors256];
                } else {
                    [self initializeColors16];
                }
            }
        }

        NSLogInfo(@"DDTTYLogger: isaColorTTY = %@", (isaColorTTY ? @"YES" : @"NO"));
        NSLogInfo(@"DDTTYLogger: isaColor256TTY: %@", (isaColor256TTY ? @"YES" : @"NO"));
        NSLogInfo(@"DDTTYLogger: isaXcodeColorTTY: %@", (isaXcodeColorTTY ? @"YES" : @"NO"));

        sharedInstance = [[self alloc] init];
    });

    return sharedInstance;
}

- (instancetype)init {
    if (sharedInstance != nil) {
        return nil;
    }

#if !defined(DD_CLI) || __has_include(<AppKit/NSColor.h>)
    if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) {
        NSLogWarn(@"CocoaLumberjack: Warning: Usage of DDTTYLogger detected when DDOSLogger is available and can be used! Please consider migrating to DDOSLogger.");
    }
#endif

    if ((self = [super init])) {
        // Initialize 'app' variable (char *)
        _appName = [[NSProcessInfo processInfo] processName];
        _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

        if (_appLen == 0) {
            _appName = @"<UnnamedApp>";
            _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        }

        _app = (char *)calloc(_appLen + 1, sizeof(char));
        if (_app == NULL) {
            return nil;
        }

        BOOL processedAppName = [_appName getCString:_app maxLength:(_appLen + 1) encoding:NSUTF8StringEncoding];
        if (!processedAppName) {
            free(_app);
            return nil;
        }

        // Initialize 'pid' variable (char *)

        _processID = [NSString stringWithFormat:@"%i", (int)getpid()];

        _pidLen = [_processID lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        _pid = (char *)calloc(_pidLen + 1, sizeof(char));

        if (_pid == NULL) {
            free(_app);
            return nil;
        }

        BOOL processedID = [_processID getCString:_pid maxLength:(_pidLen + 1) encoding:NSUTF8StringEncoding];
        if (!processedID) {
            free(_app);
            free(_pid);
            return nil;
        }

        // Initialize color stuff

        _colorsEnabled = NO;
        _colorProfilesArray = [[NSMutableArray alloc] initWithCapacity:8];
        _colorProfilesDict = [[NSMutableDictionary alloc] initWithCapacity:8];

        _automaticallyAppendNewlineForCustomFormatters = YES;
    }

    return self;
}

- (DDLoggerName)loggerName {
    return DDLoggerNameTTY;
}

- (void)loadDefaultColorProfiles {
    [self setForegroundColor:DDMakeColor(214,  57,  30) backgroundColor:nil forFlag:DDLogFlagError];
    [self setForegroundColor:DDMakeColor(204, 121,  32) backgroundColor:nil forFlag:DDLogFlagWarning];
}

- (BOOL)colorsEnabled {
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.

    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.

    DDAbstractLoggerAssertLockedPropertyAccess();
    __block BOOL result;
    dispatch_sync(DDLog.loggingQueue, ^{
        dispatch_sync(self.loggerQueue, ^{
            result = self->_colorsEnabled;
        });
    });

    return result;
}

- (void)setColorsEnabled:(BOOL)newColorsEnabled {
    __auto_type block = ^{
        @autoreleasepool {
            self->_colorsEnabled = newColorsEnabled;

            if ([self->_colorProfilesArray count] == 0) {
                [self loadDefaultColorProfiles];
            }
        }
    };

    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.

    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.

    DDAbstractLoggerAssertLockedPropertyAccess();
    dispatch_async(DDLog.loggingQueue, ^{
        dispatch_async(self.loggerQueue, block);
    });
}

- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask {
    [self setForegroundColor:txtColor backgroundColor:bgColor forFlag:mask context:LOG_CONTEXT_ALL];
}

- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask context:(NSInteger)ctxt {
    dispatch_block_t block = ^{
        @autoreleasepool {
            DDTTYLoggerColorProfile *newColorProfile = [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor
                                                                                                backgroundColor:bgColor
                                                                                                           flag:mask
                                                                                                        context:ctxt];
            if (!newColorProfile) return;

            NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile);

            NSUInteger i = 0;

            for (DDTTYLoggerColorProfile *colorProfile in self->_colorProfilesArray) {
                if ((colorProfile->mask == mask) && (colorProfile->context == ctxt)) {
                    break;
                }

                i++;
            }

            if (i < [self->_colorProfilesArray count]) {
                self->_colorProfilesArray[i] = newColorProfile;
            } else {
                [self->_colorProfilesArray addObject:newColorProfile];
            }
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forTag:(id <NSCopying>)tag {
    NSAssert([(id <NSObject>)tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag");

    __auto_type block = ^{
        @autoreleasepool {
            __auto_type newColorProfile = [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor
                                                                                   backgroundColor:bgColor
                                                                                              flag:(DDLogFlag)0
                                                                                           context:0];

            NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile);

            self->_colorProfilesDict[tag] = newColorProfile;
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)clearColorsForFlag:(DDLogFlag)mask {
    [self clearColorsForFlag:mask context:0];
}

- (void)clearColorsForFlag:(DDLogFlag)mask context:(NSInteger)context {
    __auto_type block = ^{
        @autoreleasepool {
            NSUInteger i = 0;

            for (DDTTYLoggerColorProfile *colorProfile in self->_colorProfilesArray) {
                if ((colorProfile->mask == mask) && (colorProfile->context == context)) {
                    break;
                }

                i++;
            }

            if (i < [self->_colorProfilesArray count]) {
                [self->_colorProfilesArray removeObjectAtIndex:i];
            }
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)clearColorsForTag:(id <NSCopying>)tag {
    NSAssert([(id <NSObject>) tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag");

    __auto_type block = ^{
        @autoreleasepool {
            [self->_colorProfilesDict removeObjectForKey:tag];
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)clearColorsForAllFlags {
    __auto_type block = ^{
        @autoreleasepool {
            [self->_colorProfilesArray removeAllObjects];
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)clearColorsForAllTags {
    __auto_type block = ^{
        @autoreleasepool {
            [self->_colorProfilesDict removeAllObjects];
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)clearAllColors {
    __auto_type block = ^{
        @autoreleasepool {
            [self->_colorProfilesArray removeAllObjects];
            [self->_colorProfilesDict removeAllObjects];
        }
    };

    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.

    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
        dispatch_async(DDLog.loggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)logMessage:(DDLogMessage *)logMessage {
    __auto_type logMsg = logMessage->_message;
    __auto_type isFormatted = NO;

    if (_logFormatter) {
        logMsg = [_logFormatter formatLogMessage:logMessage];
        isFormatted = logMsg != logMessage->_message;
    }

    if (logMsg) {
        // Search for a color profile associated with the log message

        DDTTYLoggerColorProfile *colorProfile = nil;

        if (_colorsEnabled) {
            if (logMessage->_representedObject) {
                colorProfile = _colorProfilesDict[logMessage->_representedObject];
            }

            if (colorProfile == nil) {
                for (DDTTYLoggerColorProfile *cp in _colorProfilesArray) {
                    if (logMessage->_flag & cp->mask) {
                        // Color profile set for this context?
                        if (logMessage->_context == cp->context) {
                            colorProfile = cp;

                            // Stop searching
                            break;
                        }

                        // Check if LOG_CONTEXT_ALL was specified as a default color for this flag
                        if (cp->context == LOG_CONTEXT_ALL) {
                            colorProfile = cp;

                            // We don't break to keep searching for more specific color profiles for the context
                        }
                    }
                }
            }
        }

        // Convert log message to C string.
        //
        // We use the stack instead of the heap for speed if possible.
        // But we're extra cautious to avoid a stack overflow.

        __auto_type msgLen = [logMsg lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        const __auto_type useStack = msgLen < (1024 * 4);

        char *msg;
        if (useStack) {
            msg = (char *)alloca(msgLen + 1);
        } else {
            msg = (char *)calloc(msgLen + 1, sizeof(char));
        }
        if (msg == NULL) {
            return;
        }

        BOOL logMsgEnc = [logMsg getCString:msg maxLength:(msgLen + 1) encoding:NSUTF8StringEncoding];
        if (!logMsgEnc) {
            if (!useStack) {
                free(msg);
            }
            return;
        }

        // Write the log message to STDERR

        if (isFormatted) {
            // The log message has already been formatted.
            const size_t maxIovecLen = 5;
            size_t iovecLen = _automaticallyAppendNewlineForCustomFormatters ? 5 : 4;
            struct iovec v[maxIovecLen] = { 0 };

            if (colorProfile) {
                v[0].iov_base = colorProfile->fgCode;
                v[0].iov_len = colorProfile->fgCodeLen;

                v[1].iov_base = colorProfile->bgCode;
                v[1].iov_len = colorProfile->bgCodeLen;

                v[maxIovecLen - 1].iov_base = colorProfile->resetCode;
                v[maxIovecLen - 1].iov_len = colorProfile->resetCodeLen;
            }

            v[2].iov_base = msg;
            v[2].iov_len = (msgLen > SIZE_MAX - 1) ? SIZE_MAX - 1 : msgLen;

            if (_automaticallyAppendNewlineForCustomFormatters && (v[2].iov_len == 0 || msg[v[2].iov_len - 1] != '\n')) {
                v[3].iov_base = "\n";
                v[3].iov_len = 1;
                iovecLen = 5;
            }

            writev(STDERR_FILENO, v, (int)iovecLen);
        } else {
            // The log message is unformatted, so apply standard NSLog style formatting.

            int len;
            char ts[24] = "";
            size_t tsLen = 0;

            // Calculate timestamp.
            // The technique below is faster than using NSDateFormatter.
            if (logMessage->_timestamp) {
                __auto_type epoch = [logMessage->_timestamp timeIntervalSince1970];
                double integral;
                __auto_type fract = modf(epoch, &integral);
                struct tm tm;
                __auto_type time = (time_t)integral;
                (void)localtime_r(&time, &tm);
                __auto_type milliseconds = (long)(fract * 1000.0);

                len = snprintf(ts, 24, "%04d-%02d-%02d %02d:%02d:%02d:%03ld", // yyyy-MM-dd HH:mm:ss:SSS
                               tm.tm_year + 1900,
                               tm.tm_mon + 1,
                               tm.tm_mday,
                               tm.tm_hour,
                               tm.tm_min,
                               tm.tm_sec, milliseconds);

                tsLen = (NSUInteger)MAX(MIN(24 - 1, len), 0);
            }

            // Calculate thread ID
            //
            // How many characters do we need for the thread id?
            // logMessage->machThreadID is of type mach_port_t, which is an unsigned int.
            //
            // 1 hex char = 4 bits
            // 8 hex chars for 32 bit, plus ending '\0' = 9

            char tid[9];
            len = snprintf(tid, 9, "%s", [logMessage->_threadID cStringUsingEncoding:NSUTF8StringEncoding]);

            __auto_type tidLen = (NSUInteger)MAX(MIN(9 - 1, len), 0);

            // Here is our format: "%s %s[%i:%s] %s", timestamp, appName, processID, threadID, logMsg

            struct iovec v[13];

            if (colorProfile) {
                v[0].iov_base = colorProfile->fgCode;
                v[0].iov_len = colorProfile->fgCodeLen;

                v[1].iov_base = colorProfile->bgCode;
                v[1].iov_len = colorProfile->bgCodeLen;

                v[12].iov_base = colorProfile->resetCode;
                v[12].iov_len = colorProfile->resetCodeLen;
            } else {
                v[0].iov_base = "";
                v[0].iov_len = 0;

                v[1].iov_base = "";
                v[1].iov_len = 0;

                v[12].iov_base = "";
                v[12].iov_len = 0;
            }

            v[2].iov_base = ts;
            v[2].iov_len = tsLen;

            v[3].iov_base = " ";
            v[3].iov_len = 1;

            v[4].iov_base = _app;
            v[4].iov_len = _appLen;

            v[5].iov_base = "[";
            v[5].iov_len = 1;

            v[6].iov_base = _pid;
            v[6].iov_len = _pidLen;

            v[7].iov_base = ":";
            v[7].iov_len = 1;

            v[8].iov_base = tid;
            v[8].iov_len = MIN((size_t)8, tidLen); // snprintf doesn't return what you might think

            v[9].iov_base = "] ";
            v[9].iov_len = 2;

            v[10].iov_base = (char *)msg;
            v[10].iov_len = msgLen;

            v[11].iov_base = "\n";
            v[11].iov_len = (msg[msgLen] == '\n') ? 0 : 1;

            writev(STDERR_FILENO, v, 13);
        }

        if (!useStack) {
            free(msg);
        }
    }
}

@end

#pragma mark -

@implementation DDTTYLoggerColorProfile

- (instancetype)initWithForegroundColor:(DDColor *)fgColor backgroundColor:(DDColor *)bgColor flag:(DDLogFlag)aMask context:(NSInteger)ctxt {
    if ((self = [super init])) {
        mask = aMask;
        context = ctxt;

        CGFloat r, g, b;

        if (fgColor) {
            [DDTTYLogger getRed:&r green:&g blue:&b fromColor:fgColor];

            fg.r = (uint8_t)(r * (CGFloat)255.0);
            fg.g = (uint8_t)(g * (CGFloat)255.0);
            fg.b = (uint8_t)(b * (CGFloat)255.0);
        }

        if (bgColor) {
            [DDTTYLogger getRed:&r green:&g blue:&b fromColor:bgColor];

            bg.r = (uint8_t)(r * (CGFloat)255.0);
            bg.g = (uint8_t)(g * (CGFloat)255.0);
            bg.b = (uint8_t)(b * (CGFloat)255.0);
        }

        if (fgColor && isaColorTTY) {
            // Map foreground color to closest available shell color

            fgCodeIndex = [DDTTYLogger codeIndexForColor:fgColor];
            fgCodeRaw   = codesFg[fgCodeIndex];

            const __auto_type escapeSeq = @"\033[";

            __auto_type len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
            __auto_type len2 = [fgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

            BOOL escapeSeqEnc = [escapeSeq getCString:(fgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding];
            BOOL fgCodeRawEsc = [fgCodeRaw getCString:(fgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding];

            if (!escapeSeqEnc || !fgCodeRawEsc) {
                return nil;
            }

            fgCodeLen = len1 + len2;
        } else if (fgColor && isaXcodeColorTTY) {
            // Convert foreground color to color code sequence
            const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ;
            __auto_type result = snprintf(fgCode, 24, "%sfg%u,%u,%u;", escapeSeq, fg.r, fg.g, fg.b);
            fgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0);
        } else {
            // No foreground color or no color support
            fgCode[0] = '\0';
            fgCodeLen = 0;
        }

        if (bgColor && isaColorTTY) {
            // Map background color to closest available shell color

            bgCodeIndex = [DDTTYLogger codeIndexForColor:bgColor];
            bgCodeRaw   = codesBg[bgCodeIndex];

            const __auto_type escapeSeq = @"\033[";

            __auto_type len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
            __auto_type len2 = [bgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

            BOOL escapeSeqEnc = [escapeSeq getCString:(bgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding];
            BOOL bgCodeRawEsc = [bgCodeRaw getCString:(bgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding];

            if (!escapeSeqEnc || !bgCodeRawEsc) {
                return nil;
            }

            bgCodeLen = len1 + len2;
        } else if (bgColor && isaXcodeColorTTY) {
            // Convert background color to color code sequence
            const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ;
            __auto_type result = snprintf(bgCode, 24, "%sbg%u,%u,%u;", escapeSeq, bg.r, bg.g, bg.b);
            bgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0);
        } else {
            // No background color or no color support
            bgCode[0] = '\0';
            bgCodeLen = 0;
        }

        if (isaColorTTY) {
            resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, "\033[0m"), 0);
        } else if (isaXcodeColorTTY) {
            resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, XCODE_COLORS_RESET), 0);
        } else {
            resetCode[0] = '\0';
            resetCodeLen = 0;
        }
    }

    return self;
}

- (NSString *)description {
    return [NSString stringWithFormat:
            @"<DDTTYLoggerColorProfile: %p mask:%i ctxt:%ld fg:%u,%u,%u bg:%u,%u,%u fgCode:%@ bgCode:%@>",
            self, (int)mask, (long)context, fg.r, fg.g, fg.b, bg.r, bg.g, bg.b, fgCodeRaw, bgCodeRaw];
}

@end
