Passed
Push — master ( 823aee...b9b37f )
by Alec
13:15 queued 12s
created

ColorMode::degradeHexColorToAnsi4()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlecRabbit\Spinner\Contract;
6
7
use AlecRabbit\Spinner\Exception\InvalidArgumentException;
8
use AlecRabbit\Spinner\Exception\LogicException;
9
10
use function strlen;
11
12
enum ColorMode: int
13
{
14
    protected const COLOR_TABLE = [
15
        0 => '#000000',
16
        1 => '#800000',
17
        2 => '#008000',
18
        3 => '#808000',
19
        4 => '#000080',
20
        5 => '#800080',
21
        6 => '#008080',
22
        7 => '#c0c0c0',
23
        8 => '#808080',
24
        9 => '#ff0000',
25
        10 => '#00ff00',
26
        11 => '#ffff00',
27
        12 => '#0000ff',
28
        13 => '#ff00ff',
29
        14 => '#00ffff',
30
        15 => '#ffffff',
31
        16 => '#000000',
32
        17 => '#00005f',
33
        18 => '#000087',
34
        19 => '#0000af',
35
        20 => '#0000d7',
36
        21 => '#0000ff',
37
        22 => '#005f00',
38
        23 => '#005f5f',
39
        24 => '#005f87',
40
        25 => '#005faf',
41
        26 => '#005fd7',
42
        27 => '#005fff',
43
        28 => '#008700',
44
        29 => '#00875f',
45
        30 => '#008787',
46
        31 => '#0087af',
47
        32 => '#0087d7',
48
        33 => '#0087ff',
49
        34 => '#00af00',
50
        35 => '#00af5f',
51
        36 => '#00af87',
52
        37 => '#00afaf',
53
        38 => '#00afd7',
54
        39 => '#00afff',
55
        40 => '#00d700',
56
        41 => '#00d75f',
57
        42 => '#00d787',
58
        43 => '#00d7af',
59
        44 => '#00d7d7',
60
        45 => '#00d7ff',
61
        46 => '#00ff00',
62
        47 => '#00ff5f',
63
        48 => '#00ff87',
64
        49 => '#00ffaf',
65
        50 => '#00ffd7',
66
        51 => '#00ffff',
67
        52 => '#5f0000',
68
        53 => '#5f005f',
69
        54 => '#5f0087',
70
        55 => '#5f00af',
71
        56 => '#5f00d7',
72
        57 => '#5f00ff',
73
        58 => '#5f5f00',
74
        59 => '#5f5f5f',
75
        60 => '#5f5f87',
76
        61 => '#5f5faf',
77
        62 => '#5f5fd7',
78
        63 => '#5f5fff',
79
        64 => '#5f8700',
80
        65 => '#5f875f',
81
        66 => '#5f8787',
82
        67 => '#5f87af',
83
        68 => '#5f87d7',
84
        69 => '#5f87ff',
85
        70 => '#5faf00',
86
        71 => '#5faf5f',
87
        72 => '#5faf87',
88
        73 => '#5fafaf',
89
        74 => '#5fafd7',
90
        75 => '#5fafff',
91
        76 => '#5fd700',
92
        77 => '#5fd75f',
93
        78 => '#5fd787',
94
        79 => '#5fd7af',
95
        80 => '#5fd7d7',
96
        81 => '#5fd7ff',
97
        82 => '#5fff00',
98
        83 => '#5fff5f',
99
        84 => '#5fff87',
100
        85 => '#5fffaf',
101
        86 => '#5fffd7',
102
        87 => '#5fffff',
103
        88 => '#870000',
104
        89 => '#87005f',
105
        90 => '#870087',
106
        91 => '#8700af',
107
        92 => '#8700d7',
108
        93 => '#8700ff',
109
        94 => '#875f00',
110
        95 => '#875f5f',
111
        96 => '#875f87',
112
        97 => '#875faf',
113
        98 => '#875fd7',
114
        99 => '#875fff',
115
        100 => '#878700',
116
        101 => '#87875f',
117
        102 => '#878787',
118
        103 => '#8787af',
119
        104 => '#8787d7',
120
        105 => '#8787ff',
121
        106 => '#87af00',
122
        107 => '#87af5f',
123
        108 => '#87af87',
124
        109 => '#87afaf',
125
        110 => '#87afd7',
126
        111 => '#87afff',
127
        112 => '#87d700',
128
        113 => '#87d75f',
129
        114 => '#87d787',
130
        115 => '#87d7af',
131
        116 => '#87d7d7',
132
        117 => '#87d7ff',
133
        118 => '#87ff00',
134
        119 => '#87ff5f',
135
        120 => '#87ff87',
136
        121 => '#87ffaf',
137
        122 => '#87ffd7',
138
        123 => '#87ffff',
139
        124 => '#af0000',
140
        125 => '#af005f',
141
        126 => '#af0087',
142
        127 => '#af00af',
143
        128 => '#af00d7',
144
        129 => '#af00ff',
145
        130 => '#af5f00',
146
        131 => '#af5f5f',
147
        132 => '#af5f87',
148
        133 => '#af5faf',
149
        134 => '#af5fd7',
150
        135 => '#af5fff',
151
        136 => '#af8700',
152
        137 => '#af875f',
153
        138 => '#af8787',
154
        139 => '#af87af',
155
        140 => '#af87d7',
156
        141 => '#af87ff',
157
        142 => '#afaf00',
158
        143 => '#afaf5f',
159
        144 => '#afaf87',
160
        145 => '#afafaf',
161
        146 => '#afafd7',
162
        147 => '#afafff',
163
        148 => '#afd700',
164
        149 => '#afd75f',
165
        150 => '#afd787',
166
        151 => '#afd7af',
167
        152 => '#afd7d7',
168
        153 => '#afd7ff',
169
        154 => '#afff00',
170
        155 => '#afff5f',
171
        156 => '#afff87',
172
        157 => '#afffaf',
173
        158 => '#afffd7',
174
        159 => '#afffff',
175
        160 => '#d70000',
176
        161 => '#d7005f',
177
        162 => '#d70087',
178
        163 => '#d700af',
179
        164 => '#d700d7',
180
        165 => '#d700ff',
181
        166 => '#d75f00',
182
        167 => '#d75f5f',
183
        168 => '#d75f87',
184
        169 => '#d75faf',
185
        170 => '#d75fd7',
186
        171 => '#d75fff',
187
        172 => '#d78700',
188
        173 => '#d7875f',
189
        174 => '#d78787',
190
        175 => '#d787af',
191
        176 => '#d787d7',
192
        177 => '#d787ff',
193
        178 => '#d7af00',
194
        179 => '#d7af5f',
195
        180 => '#d7af87',
196
        181 => '#d7afaf',
197
        182 => '#d7afd7',
198
        183 => '#d7afff',
199
        184 => '#d7d700',
200
        185 => '#d7d75f',
201
        186 => '#d7d787',
202
        187 => '#d7d7af',
203
        188 => '#d7d7d7',
204
        189 => '#d7d7ff',
205
        190 => '#d7ff00',
206
        191 => '#d7ff5f',
207
        192 => '#d7ff87',
208
        193 => '#d7ffaf',
209
        194 => '#d7ffd7',
210
        195 => '#d7ffff',
211
        196 => '#ff0000',
212
        197 => '#ff005f',
213
        198 => '#ff0087',
214
        199 => '#ff00af',
215
        200 => '#ff00d7',
216
        201 => '#ff00ff',
217
        202 => '#ff5f00',
218
        203 => '#ff5f5f',
219
        204 => '#ff5f87',
220
        205 => '#ff5faf',
221
        206 => '#ff5fd7',
222
        207 => '#ff5fff',
223
        208 => '#ff8700',
224
        209 => '#ff875f',
225
        210 => '#ff8787',
226
        211 => '#ff87af',
227
        212 => '#ff87d7',
228
        213 => '#ff87ff',
229
        214 => '#ffaf00',
230
        215 => '#ffaf5f',
231
        216 => '#ffaf87',
232
        217 => '#ffafaf',
233
        218 => '#ffafd7',
234
        219 => '#ffafff',
235
        220 => '#ffd700',
236
        221 => '#ffd75f',
237
        222 => '#ffd787',
238
        223 => '#ffd7af',
239
        224 => '#ffd7d7',
240
        225 => '#ffd7ff',
241
        226 => '#ffff00',
242
        227 => '#ffff5f',
243
        228 => '#ffff87',
244
        229 => '#ffffaf',
245
        230 => '#ffffd7',
246
        231 => '#ffffff',
247
        232 => '#080808',
248
        233 => '#121212',
249
        234 => '#1c1c1c',
250
        235 => '#262626',
251
        236 => '#303030',
252
        237 => '#3a3a3a',
253
        238 => '#444444',
254
        239 => '#4e4e4e',
255
        240 => '#585858',
256
        241 => '#626262',
257
        242 => '#6c6c6c',
258
        243 => '#767676',
259
        244 => '#808080',
260
        245 => '#8a8a8a',
261
        246 => '#949494',
262
        247 => '#9e9e9e',
263
        248 => '#a8a8a8',
264
        249 => '#b2b2b2',
265
        250 => '#bcbcbc',
266
        251 => '#c6c6c6',
267
        252 => '#d0d0d0',
268
        253 => '#dadada',
269
        254 => '#e4e4e4',
270
        255 => '#eeeeee',
271
    ];
272
273
    case NONE = 0;
274
    case ANSI4 = 16;
275
    case ANSI8 = 256;
276
    case ANSI24 = 65535;
277
278
    /**
279
     * @throws LogicException
280
     * @throws InvalidArgumentException
281
     */
282
    public function ansiCode(int|string $color): string
283
    {
284
        $this->assertColor($color);
285
286
        $color24 = (string)$color;
287
288
        return match ($this) {
289
            self::ANSI4 => $this->convert4($color),
290
            self::ANSI8 => $this->convert8($color),
291
            self::ANSI24 => $this->convert24($color24),
292
            default => throw new LogicException(
293
                sprintf(
294
                    '%s::%s: Unable to convert "%s" to ansi code.',
295
                    self::class,
296
                    $this->name,
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on AlecRabbit\Spinner\Contract\ColorMode. Did you maybe forget to declare it?
Loading history...
297
                    $color
298
                )
299
            ),
300
        };
301
    }
302
303
    /**
304
     * @throws InvalidArgumentException
305
     */
306
    protected function assertColor(int|string $color): void
307
    {
308
        match (true) {
309
            is_int($color) => $this->assertIntColor($color),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->assertIntColor($color) targeting AlecRabbit\Spinner\Contr...rMode::assertIntColor() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
It seems like $color can also be of type string; however, parameter $color of AlecRabbit\Spinner\Contr...rMode::assertIntColor() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
            is_int($color) => $this->assertIntColor(/** @scrutinizer ignore-type */ $color),
Loading history...
310
            is_string($color) => $this->assertStringColor($color),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->assertStringColor($color) targeting AlecRabbit\Spinner\Contr...de::assertStringColor() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
311
        };
312
    }
313
314
    /**
315
     * @throws InvalidArgumentException
316
     */
317
    private function assertIntColor(int $color): void
318
    {
319
        match (true) {
320
            0 > $color => throw new InvalidArgumentException(
321
                sprintf(
322
                    'Value should be positive integer, %d given.',
323
                    $color
324
                )
325
            ),
326
            self::ANSI24->name === $this->name => throw new InvalidArgumentException(
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on AlecRabbit\Spinner\Contract\ColorMode. Did you maybe forget to declare it?
Loading history...
327
                sprintf(
328
                    'For %s::%s color mode rendering from int is not allowed.',
329
                    self::class,
330
                    self::ANSI24->name
331
                )
332
            ),
333
            self::ANSI8->name === $this->name && 255 < $color => throw new InvalidArgumentException(
334
                sprintf(
335
                    'For %s::%s color mode value should be in range 0..255, %d given.',
336
                    self::class,
337
                    self::ANSI8->name,
338
                    $color
339
                )
340
            ),
341
            self::ANSI4->name === $this->name && 16 < $color => throw new InvalidArgumentException(
342
                sprintf(
343
                    'For %s::%s color mode value should be in range 0..15, %d given.',
344
                    self::class,
345
                    self::ANSI4->name,
346
                    $color
347
                )
348
            ),
349
            default => null,
350
        };
351
    }
352
353
    /**
354
     * @throws InvalidArgumentException
355
     */
356
    protected function assertStringColor(string $entry): void
357
    {
358
        $strlen = strlen($entry);
359
        match (true) {
360
            0 === $strlen => throw new InvalidArgumentException(
361
                'Value should not be empty string.'
362
            ),
363
            !str_starts_with($entry, '#') => throw new InvalidArgumentException(
364
                sprintf(
365
                    'Value should be a valid hex color code("#rgb", "#rrggbb"), "%s" given. No "#" found.',
366
                    $entry
367
                )
368
            ),
369
            4 !== $strlen && 7 !== $strlen => throw new InvalidArgumentException(
370
                sprintf(
371
                    'Value should be a valid hex color code("#rgb", "#rrggbb"), "%s" given. Length: %d.',
372
                    $entry,
373
                    $strlen
374
                )
375
            ),
376
            default => null,
377
        };
378
    }
379
380
    /**
381
     * @throws InvalidArgumentException
382
     */
383
    protected function convert4(int|string $color): string
384
    {
385
        if (is_int($color)) {
386
            return (string)$color;
387
        }
388
        return $this->convertFromHexToAnsiColorCode($color);
389
    }
390
391
    /**
392
     * @throws InvalidArgumentException
393
     */
394
    private function convertFromHexToAnsiColorCode(string $hexColor): string
395
    {
396
        $hexColor = str_replace('#', '', $hexColor);
397
398
        if (3 === strlen($hexColor)) {
399
            $hexColor = $hexColor[0] . $hexColor[0] . $hexColor[1] . $hexColor[1] . $hexColor[2] . $hexColor[2];
400
        }
401
402
        if (6 !== strlen($hexColor)) {
403
            throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor));
404
        }
405
406
        $color = hexdec($hexColor);
407
408
        $r = ($color >> 16) & 255;
409
        $g = ($color >> 8) & 255;
410
        $b = $color & 255;
411
412
        return match ($this) {
413
            self::ANSI4 => (string)$this->convertFromRGB($r, $g, $b),
414
            self::ANSI8 => '8;5;' . ((string)$this->convertFromRGB($r, $g, $b)),
415
            self::ANSI24 => sprintf('8;2;%d;%d;%d', $r, $g, $b),
416
            self::NONE => throw new InvalidArgumentException('Hex color cannot be converted to NONE.'),
417
        };
418
    }
419
420
    /**
421
     * @throws InvalidArgumentException
422
     */
423
    private function convertFromRGB(int $r, int $g, int $b): int
424
    {
425
        return match ($this) {
426
            self::ANSI4 => $this->degradeHexColorToAnsi4($r, $g, $b),
427
            self::ANSI8 => $this->degradeHexColorToAnsi8($r, $g, $b),
428
            default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.")
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on AlecRabbit\Spinner\Contract\ColorMode. Did you maybe forget to declare it?
Loading history...
429
        };
430
    }
431
432
    private function degradeHexColorToAnsi4(int $r, int $g, int $b): int
433
    {
434
        /** @psalm-suppress TypeDoesNotContainType */
435
        if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
436
            return 0;
437
        }
438
439
        return (int)((round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255));
440
    }
441
442
    private function getSaturation(int $r, int $g, int $b): int
443
    {
444
        $r = $r / 255;
445
        $g = $g / 255;
446
        $b = $b / 255;
447
        $v = max($r, $g, $b);
448
449
        if (0 === $diff = $v - min($r, $g, $b)) {
450
            return 0;
451
        }
452
453
        return (int)((int)$diff * 100 / $v);
454
    }
455
456
    /**
457
     * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license).
458
     */
459
    private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
460
    {
461
        if ($r === $g && $g === $b) {
462
            if ($r < 8) {
463
                return 16;
464
            }
465
466
            if ($r > 248) {
467
                return 231;
468
            }
469
470
            return (int)round(($r - 8) / 247 * 24) + 232;
471
        } else {
472
            return 16 +
473
                (36 * (int)round($r / 255 * 5)) +
474
                (6 * (int)round($g / 255 * 5)) +
475
                (int)round($b / 255 * 5);
476
        }
477
    }
478
479
    /**
480
     * @throws InvalidArgumentException
481
     */
482
    protected function convert8(int|string $color): string
483
    {
484
        if (is_int($color)) {
485
            return '8;5;' . $color;
486
        }
487
488
        /** @var null|array<int, string> $colors8 */
489
        static $colors8 = null;
490
491
        if (null === $colors8) {
492
            $colors8 = array_slice(self::COLOR_TABLE, 16, preserve_keys: true);
0 ignored issues
show
Bug introduced by
The constant AlecRabbit\Spinner\Contract\ColorMode::COLOR_TABLE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
493
        }
494
495
        /** @var int|false $result */
496
        $result =
497
            // non-optimal code, but it's not a bottleneck
498
            array_search(
499
                $color,
500
                $colors8,
501
                true
502
            );
503
504
505
        if (false === $result) {
0 ignored issues
show
introduced by
The condition false === $result is always false.
Loading history...
506
            return $this->convertFromHexToAnsiColorCode($color);
507
        }
508
509
        return '8;5;' . (string)$result;
510
    }
511
512
    /**
513
     * @throws InvalidArgumentException
514
     */
515
    protected function convert24(string $color): string
516
    {
517
        return $this->convertFromHexToAnsiColorCode($color);
518
    }
519
520
    public function simplest(self $other): self
521
    {
522
        if ($this->value <= $other->value) {
523
            return $this;
524
        }
525
        return $other;
526
    }
527
}
528