<?php

namespace onespace\tools\rabbit\helpers;

/**
 * Various helper methods to improve CLI feedback
 *
 * @author Gareth Palmer <gareth@one-space.co.za>
 */
trait CLIHelper {
    public bool $silent = false;

    /*
          ____                           _
         / ___| ___ _ __   ___ _ __ __ _| |
        | |  _ / _ \ '_ \ / _ \ '__/ _` | |
        | |_| |  __/ | | |  __/ | | (_| | |
         \____|\___|_| |_|\___|_|  \__,_|_|
    */

    /**
     * Write one or more linebreaks to the screen
     */
    protected function writeLinebreak($num = 1): void {
        $string = "";
        for ($i = 0; $i < $num; $i++) {
            $string .= PHP_EOL;
        }
        print_r($string);
    }

    /**
     * Write to the screen a parsed string _WITHOUT_ a trailing line break.
     */
    protected function write(string $string, Log $log = Log::NONE, string $key = __METHOD__): void {
        $log->yiiLog($string, $key);
        if ($this->silent) {
            return;
        }

        if ($log === Log::DEBUG) {
            if (YII_DEBUG) {
                print_r($log->level() . $string);
            }
        } else {
            print_r($log->level() . $string);
        }
    }

    /**
     * Write to the screen a parsed string _WITH_ a trailing line break.
     */
    protected function writeln(string $string, Log $log = Log::NONE, string $key = __METHOD__): void {
        $log->yiiLog($string, $key);
        if ($this->silent) {
            return;
        }

        if ($log === Log::DEBUG) {
            if (YII_DEBUG) {
                print_r($log->level() . $string . PHP_EOL);
            }
        } else {
            print_r($log->level() . $string . PHP_EOL);
        }
    }

    /**
     * Write to the screen an object or array of data, formatted in a readable way.
     */
    protected function writeData(array|object $data, Log $log = Log::NONE, string $key = __METHOD__): void {
        $log->yiiLog($data, $key);
        if ($this->silent) {
            return;
        }

        if ($log === Log::DEBUG) {
            if (YII_DEBUG) {
                $this->writeln($log->level());
                print_r($data);
            }
        } else {
            $this->writeln($log->level());
            print_r($data);
        }
    }

    /**
     * Write "true" or "false" to the screen based on a parsed bool value _WITHOUT_ a trailing line break.
     */
    protected function writeBool(bool $bool, Log $log = Log::NONE, string $key = __METHOD__): void {
        if ($bool) {
            $this->write('true', $log, $key);
        } else {
            $this->write('false', $log, $key);
        }
    }

    /**
     * Write "true" or "false" to the screen based on a parsed bool value _WITH_ a trailing line break.
     */
    protected function writelnBool(bool $bool, Log $log = Log::NONE, string $key = __METHOD__): void {
        if ($bool) {
            $this->writeln('true', $log, $key);
        } else {
            $this->writeln('false', $log, $key);
        }
    }

    /**
     * Write a "✔" or "✘" to the screen based on a parsed bool value _WITHOUT_ a trailing line break.
     */
    protected function writeCheck(bool $bool, Log $log = Log::NONE, string $key = __METHOD__): void {
        if ($bool) {
            $this->write('✔', $log, $key);
        } else {
            $this->write('✘', $log, $key);
        }
    }

    /**
     * Write a "✔" or "✘" to the screen based on a parsed bool value _WITH_ a trailing line break.
     */
    protected function writelnCheck(bool $bool, Log $log = Log::NONE, string $key = __METHOD__): void {
        if ($bool) {
            $this->writeln('✔', $log, $key);
        } else {
            $this->writeln('✘', $log, $key);
        }
    }

    /**
     * Write a date & time Timestamp _WITHOUT_ a trailing line break.
     */
    protected function writeTs(string $format = 'Y-m-d H:i:s', Log $log = Log::NONE, string $key = __METHOD__): void {
        $this->write(date($format), $log, $key);
    }

    /**
     * Write a date & time Timestamp _WITH_ a trailing line break.
     */
    protected function writelnTs(string $format = 'Y-m-d H:i:s', Log $log = Log::NONE, string $key = __METHOD__): void {
        $this->writeln(date($format), $log, $key);
    }

    /**
     * Draw a series of `.` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawDotLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('.', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /**
     * Draw a series of `-` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawDashLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('-', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /**
     * Draw a series of `+` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawPlusLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('+', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /**
     * Draw a series of `*` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawStarLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('*', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /**
     * Draw a series of `_` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawUnderscoreLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('_', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /**
     * Draw a series of `#` to make a line.
     *
     * @param   int $count  The number of characters in the line.
     * @param   string|null $wrapper    A wrapping character or characters to put at
     *                                  the beginning and end of the line, for example `|`.
     *                                  Set to null to skip. Default: `null`
     */
    protected function drawHashLine(int $count = 22, ?string $wrapper = null, Log $log = Log::NONE, string $key = __METHOD__): void {
        $txt = "";
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $txt .= $this->repeatChar('#', $count);
        if ($wrapper !== null) {
            $txt .= $wrapper;
        }
        $this->writeln($txt, $log, $key);
    }

    /*
         _____     _     _
        |_   _|_ _| |__ | | ___
          | |/ _` | '_ \| |/ _ \
          | | (_| | |_) | |  __/
          |_|\__,_|_.__/|_|\___|
    */

    private function repeatChar(string $char, int $count): string {
        $str = '';
        for ($i = 0; $i < $count; $i++) {
            $str .= $char;
        }
        return $str;
    }

    private function setMaxLen(array &$maxLen, string $heading, string $value): void {
        $len = strlen($value);
        if (!isset($maxLen[$heading])) {
            $maxLen[$heading] = $len;
            return;
        }
        if ($len > $maxLen[$heading]) {
            $maxLen[$heading] = $len;
        }
    }

    /**
     * Draw out a table on the CLI of parsed headings and values.
     */
    protected function table(array $headings, array $values, Log $log = Log::NONE, string $key = __METHOD__): void {
        $maxLen = [];
        foreach ($headings as $heading) {
            $maxLen[$heading] = strlen($heading);
        }
        foreach ($values as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $k => $v) {
                    $this->setMaxLen($maxLen, $k, $v);
                }
            } else {
                $this->setMaxLen($maxLen, $key, $value);
            }
        }

        $str = '';

        foreach ($headings as $heading) {
            $str .= "| {$heading} ";
            if (strlen($heading) < $maxLen[$heading]) {
                $str .= $this->repeatChar(' ', $maxLen[$heading] - strlen($heading));
            }
        }
        $str .= "|\n";

        $str .= "|";
        foreach ($headings as $heading) {
            $str .= $this->repeatChar('-', $maxLen[$heading] + 2);
            $str .= '+';
        }
        $str = substr_replace($str, "|\n", -1);

        $multi = false;
        foreach ($values as $key => $value) {
            if (is_array($value)) {
                $multi = true;
                foreach ($value as $k => $v) {
                    $str .= "| {$v} ";
                    $str .= $this->repeatChar(' ', $maxLen[$k] - strlen($v));
                }
                $str .= "|\n";
            } else {
                $str .= "| {$value} ";
                $str .= $this->repeatChar(' ', $maxLen[$key] - strlen($value));
            }
        }
        if (!$multi) {
            $str .= "|\n";
        }
        $this->writeln($str, $log, $key);
    }

    /*
         ____            _        _            __
        |  _ \ _ __ ___ | |_ ___ | |__  _   _ / _|
        | |_) | '__/ _ \| __/ _ \| '_ \| | | | |_
        |  __/| | | (_) | || (_) | |_) | |_| |  _|
        |_|   |_|  \___/ \__\___/|_.__/ \__,_|_|
    */

    /**
     * In debug mode, print the data to the screen.
     */
    protected function printProtoFile(\Google\Protobuf\Internal\Message $bin, Log $log = Log::DEBUG, $key = __METHOD__): void {
        $this->writeln($bin->serializeToJsonString() . PHP_EOL, $log, $key);
        $this->writeln(bin2hex($bin->serializeToString()) . PHP_EOL, $log, $key);
    }

    /**
     * Assumes we're in a service handler and $this->body exists. This prints out `$this->body`
     */
    protected function writeBody(Log $log = Log::NONE, string $key = __METHOD__): void {
        if (!isset($this->body)) {
            return;
        }
        if (gettype($this->body) === "array" || $this->body instanceof \stdClass) {
            $this->writeData($this->body, $log, $key);
        } else {
            $this->writeData(json_decode($this->body->rawPacket->serializeToJsonString()), $log, $key);
        }
    }

    /*
         _____ _
        |_   _(_)_ __ ___   ___ _ __
          | | | | '_ ` _ \ / _ \ '__|
          | | | | | | | | |  __/ |
          |_| |_|_| |_| |_|\___|_|
    */

    /**
     * The precise microsecond the task started.
     *
     * @var int $startTime
     *
     * @access  private
     */

    private float $startTime;

    /**
     * The precise microsecond the task started.
     *
     * @var int $endTime
     *
     * @access  private
     */

    private float $endTime;

    /**
     * Start the timer.
     *
     * @access  protected
     */
    protected function startTimer(): void {
        $this->startTime = microtime(true);
    }

    /**
     * End the timer.
     *
     * @access  protected
     */
    protected function endTimer(): void {
        $this->endTime = microtime(true);
    }

    /**
     * Resets the timer.
     *
     * @access  protected
     */
    protected function resetTimer(): void {
        unset($this->startTime, $this->endTime);
    }

    /**
     * Returns the duration of the task as a float.
     *
     * @param   int|null    $round  The number of decimal places to round to. Set to null to skip. Default: null.
     *
     * @return  float
     *
     * @access  protected
     */
    protected function totalDuration(?int $round = null): float {
        if ($round === null) {
            return $this->endTime - $this->startTime;
        }
        return round($this->endTime - $this->startTime, $round);
    }

    /**
     * Echos a textual summary of the duration of the task.
     *
     * @param   int $round  The number of decimal places to round to. Default: 4.
     * @param   string  $lb The lib
     *
     * @access  protected
     */
    protected function timerSummary(int $round = 4, string $lb = PHP_EOL): void {
        if ($this->silent) {
            return;
        }
        echo sprintf("The task took %f seconds to complete.%s", $this->totalDuration($round), $lb);
    }

    /*
         ____        _
        / ___| _ __ (_)_ __  _ __   ___ _ __
        \___ \| '_ \| | '_ \| '_ \ / _ \ '__|
         ___) | |_) | | | | | | | |  __/ |
        |____/| .__/|_|_| |_|_| |_|\___|_|
              |_|
    */

    /**
     * The total number of items the task will process.
     *
     * @var int $spinnerTotal
     *
     * @access  private
     */

    private int $spinnerTotal;

    /**
     * Sets the total the spinner needs for user feedback.
     *
     * @var int $spinnerTotal   The total entries the spinner will spin over
     *
     * @access  protected
     */
    protected function initSpinner(int $spinnerTotal): void {
        $this->spinnerTotal = $spinnerTotal;
    }

    /**
     * Updates and draws the progress onto the screen.
     *
     * If `$this->spinnerTotal` is not set, only the spinner is shown,
     * otherwise a message `Processing: x / y` will also be shown.
     *
     * @param   int $index  The position within the loop which determines the state of the spinner.
     *
     * @access  protected
     */
    protected function spinnerNext(int $index): void {
        if ($this->silent) {
            return;
        }

        if (!isset($this->spinnerTotal)) {
            echo sprintf("\r%s", $this->getSpinnerState($index % 4));
        } else {
            echo sprintf("\rProcessing: %d / %d complete [%s]", $index, $this->spinnerTotal, $this->getSpinnerState($index % 4));
        }
    }

    /**
     * Returns the actual spinner characted according to the index.
     *
     * @param   int $index  The position within the loop which determines the state of the spinner.
     *
     * @return  string  One of the following: `|`, `\`, `-` or `/`.
     *
     * @access  public
     */
    private function getSpinnerState(int $index): string {
        $spinnerState = ['|', '\\', '-', '/'];
        return $spinnerState[$index % 4];
    }
}
