<?php

namespace nadar\quill;

/**
 * Line Object.
 *
 * A line object represents a line from the delta input. Lines are splited in the lexer object.
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
class Line
{
    /**
     * @var integer The status of a line which is not picked or done, which is default.
     */
    public const STATUS_CLEAN = 1;

    /**
     * @var integer The status of the line if its picked by a listener
     */
    public const STATUS_PICKED = 2;

    /**
     * @var integer The status of the line if some of the listener marked this line as done.
     */
    public const STATUS_DONE = 3;

    /**
     * @var array<string> An array with values which can be prependend to the actuall input string. This is mainly used if inline
     * elements are passed to the next "not" inline element.
     */
    public $prepend = [];

    /**
     * @var string The output is the value which will actually rendered by the lexer. So lines which directly write to the output
     * buffer needs to fill in this variable.
     */
    public $output;

    /**
     * @var string The input string which is assigned from the line parser. This is the actual content of the line itself!
     * @since 3.1.0
     */
    protected $input;

    /**
     * @var integer Holds the current status of the line.
     */
    protected $status = 1;

    /**
     * @var integer The ID/Index/Row of the line
     */
    protected $index;

    /**
     * @var array<mixed> An array with all attributes which are assigned to this lines. attribute can be inline markers like
     * bold, italic, links and so on.
     */
    protected $attributes = [];

    /**
     * @var Lexer The lexer object in order to access other lines and elements.
     */
    protected $lexer;

    /**
     * @var boolean Whether the current line is handled as "inline-line" or not. Inline lines have different effects when parsing the
     * end output. For example those can be skipped as they usual prepend the input value into the next line.
     */
    protected $isInline = false;

    /**
     * @var boolean Whether the current line is already escaped by a listener. If this is false, the next listener should preferable do so.
     * If this is true, it should not be done again by a next listener.
     * @since 1.2.0
     */
    protected $isEscaped = false;

    /**
     * @var boolean As certain elements has an end of newline but those are removed within the lexer opt to line methods we remember
     * this information here. If true this element has an \n element which has been original removed from input (as lines are spliited into
     * new lines).
     */
    protected $hadEndNewline = false;

    /**
     * @var boolean Whether this line has a newline or not, this information is already provided by the lines to ops method.
     */
    protected $hasNewline;

    /**
     * @var array<string>
     */
    private $_debug = [];

    /**
     * Constructor
     *
     * @param integer $index The numberic index of the row within all the lines.
     * @param string $input The input value from the line parser for the current line.
     * @param array<mixed> $attributes
     * @param boolean $hadEndNewline Whether this element orignali had an newline at the end.
     * @param boolean $hasNewline
     */
    public function __construct($index, $input, array $attributes, Lexer $lexer, $hadEndNewline, $hasNewline)
    {
        $this->index = $index;
        $this->input = $input;
        $this->attributes = $attributes;
        $this->lexer = $lexer;
        $this->hadEndNewline = $hadEndNewline;
        $this->hasNewline = $hasNewline;
    }

    /**
     * Whether the current line had a new line char or not, this is very important in terms of finding out wether its a block
     * element or inline element.
     *
     * This informations is assigned in the opsToLine() method in the lexer object.
     *
     * @return boolean
     */
    public function hasNewline()
    {
        return $this->hasNewline;
    }

    /**
     * Whether this line as an end newline char or not.
     *
     * @return boolean
     */
    public function hasEndNewline()
    {
        return $this->hadEndNewline;
    }

    /**
     * Whether this line is the first line or not.
     *
     * @return boolean
     */
    public function isFirst()
    {
        return $this->previous() === false;
    }

    /**
     * Get the Lexer
     *
     * @since 1.2.0
     * @return Lexer
     */
    public function getLexer()
    {
        return $this->lexer;
    }

    /**
     * Get the line's input in a safe way.
     *
     * Escaping for html is done if this wasn't done by a previous listener already.
     *
     * @since 1.2.0
     * @return string
     */
    public function getInput()
    {
        if ($this->isEscaped()) {
            return $this->input;
        }

        return $this->lexer->escape($this->getUnsafeInput());
    }

    /**
     * Get the raw line's input, this might not be escaped for html context.
     *
     * > Note it could be escaped if a previous inline listener updated the input value
     *
     * @since 1.2.0
     * @return string
     */
    public function getUnsafeInput()
    {
        return $this->input;
    }

    /**
     * Get the array with attributes, if any.
     *
     * @return array<mixed>
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Whether the current line has attribute informations or not.
     *
     * @return boolean
     */
    public function hasAttributes()
    {
        return !empty($this->attributes);
    }

    /**
     * Get the value for a given attribute name, if not exists return false.
     *
     * @param string $name
     * @param mixed $defaultValue (@since 3.2.0)
     * @return mixed
     */
    public function getAttribute($name, $defaultValue = false)
    {
        return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $defaultValue;
    }

    /**
     * Add a new value to the prepend array.
     *
     * Certain elements needs to prepend values into the next element. The Line argument is required in order to
     * ensure the correct index for the prepend element.
     *
     * @param string $value The value to prepend.
     * @param Line $line The line which does the prepend, this is used to ensure the correctly order index of the elements. {@since 1.3.2}
     * @return void
     */
    public function addPrepend($value, Line $line)
    {
        $this->prepend[$line->getIndex()] = $value;
    }

    /**
     * Returns the string for the prepend values.
     *
     * @return string The prepend value for this line.
     */
    public function renderPrepend()
    {
        ksort($this->prepend);
        return implode("", array_unique($this->prepend));
    }

    /**
     * While trough lines forward or backwards define trough index until false is returned.
     *
     * An example how to while trough lines, increasing (down) the index until a certain condition
     * ($line->isFirst) happens writing lint input into a buffer variable.
     *
     * > Keep in mind that while() will contain the line where the function applys, so the first line will always be
     * > the line you apply the while() function.
     *
     * ```php
     * $buffer = null;
     *
     * $line->while(function (&$index, Line $line) use (&$buffer) {
     *     $index++;
     *     $buffer.= $line->input;
     *
     *     if ($line->isFirst()) {
     *         return false;
     *     }
     * });
     *
     * echo $buffer;
     * ```
     *
     * > Keep in mind that `false` must be returned to stop the while process.
     *
     * @param callable $condition A callable which requires 2 params, the first is the index which is passed as reference,
     * second is the current line.
     * @return void
     */
    public function while(callable $condition)
    {
        $iterate = true;
        $i = $this->getIndex();
        while ($iterate) {
            $line = $this->lexer->getLine($i);

            if (!$line) {
                $iterate = false;
                break;
            }

            $iterate = call_user_func_array($condition, [&$i, $line]);

            if ($iterate !== false) {
                $iterate = true;
            }
        }
    }

    /**
     * While loop down (to the next elements) until false is returend.
     *
     * > This method wont return the line.
     *
     * @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
     * @since 1.3.0
     * @return void
     */
    public function whileNext(callable $condition)
    {
        $next = $this->next();
        if ($next) {
            $next->while(static function (&$index, Line $line) use ($condition) {
                ++$index;
                return call_user_func($condition, $line);
            });
        }
    }

    /**
     * While loop up (to the previous elements) until false is returend.
     *
     * > This method wont return the line.
     *
     * @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
     * @since 1.3.0
     * @return void
     * @see see while() for better documentation
     */
    public function whilePrevious(callable $condition)
    {
        $previous = $this->previous();
        if ($previous) {
            $previous->while(static function (&$index, Line $line) use ($condition) {
                --$index;
                return call_user_func($condition, $line);
            });
        }
    }

    /**
     * Iteration helper the go forward and backward in lines.
     *
     * The condition contains whether index should go up or down.
     *
     * ```php
     * return $this->iterate($line, function ($i) {
     *    return $i+1;
     * }, function(Line $line) {
     *      // will stop the process and return this current line
     *      return true;
     * });
     * ```
     *
     * @param Line $line
     * @param callable $condition The condition callable for the index
     * @param callable $fn The function which is returend to determine whether this line should be picked or not.
     * @return boolean|Line
     */
    protected function iterate(Line $line, callable $condition, callable $fn)
    {
        $i = $line->getIndex();
        $iterate = true;
        $response = false;
        while ($iterate) {
            $i = call_user_func($condition, $i);
            $elmn = $this->lexer->getLine($i);
            // no next element found
            if (!$elmn) {
                $iterate = false;
            } elseif (call_user_func($fn, $elmn)) {
                // fn match (return true) return current element.
                $response = $elmn;
                $iterate = false;
            }
        }

        return $response;
    }

    /**
     * Get the next element.
     *
     * If a closure is provided you can define a condition of whether next element should be taken or not.
     *
     * For example you can iterate to the next element which is not empty:
     *
     * ```php
     * $nextNotEmpty = $line->next(function(Line $line) {
     *     return !$line->isEmpty();
     * });
     * ```
     *
     * if true is returned this line will be assigned.
     *
     * @param callable $fn A function in order to determined whether this is the next element or not, if not provided the first next element is returned.
     * @return Line|boolean The method will return either a Line object or false, if there is no next Line.
     */
    public function next($fn = null)
    {
        if ($fn === null) {
            return $this->lexer->getLine($this->index + 1);
        }

        return $this->iterate($this, static function ($i) {
            return $i+1;
        }, $fn);
    }

    /**
     * Get the previous line.
     *
     * If no previous line exists, false is returned.
     *
     * ```php
     * $nextNotEmpty = $line->previous(function(Line $line) {
     *     return !$line->isEmpty();
     * });
     * ```
     *
     * if true is returned this line will be assigned.
     *
     * @param callable $fn A function in order to determined whether this is the previous element or not, if not provided the first previous element is returned.
     * @return Line|boolean The method will return either a Line object or false, if there is no next Line.
     */
    public function previous($fn = null)
    {
        if ($fn === null) {
            return $this->lexer->getLine($this->index - 1);
        }

        return $this->iterate($this, static function ($i) {
            return $i-1;
        }, $fn);
    }

    /**
     * Adjust the line's input.
     *
     * The new input is assumed to be escaped.
     *
     * @since 3.1.0
     * @param string $newInput
     * @return void
     */
    public function setInput($newInput)
    {
        $this->input = $newInput;
    }

    /**
     * Setter method whether the current element is inline or not.
     *
     * @return void
     */
    public function setAsInline()
    {
        $this->isInline = true;
    }

    /**
     * Whether current line is an inline line or not.
     */
    public function isInline(): bool
    {
        return $this->isInline;
    }

    /**
     * Setter method whether the current line is escaped or not.
     *
     * @since 1.2.0
     * @return void
     */
    public function setAsEscaped()
    {
        $this->isEscaped = true;
    }

    /**
     * Whether the current line is escaped or not.
     *
     * @since 1.2.0
     * @return boolean
     */
    public function isEscaped()
    {
        return $this->isEscaped;
    }

    /**
     * Getter method for the index of the line.
     *
     * @return integer
     */
    public function getIndex()
    {
        return $this->index;
    }

    /**
     * Set this line as picked.
     *
     * @return void
     */
    public function setPicked()
    {
        $this->status = self::STATUS_PICKED;
    }

    /**
     * Whether current line is picked or not. If the line has been picked an is marked as
     * done, is picked will return false.
     *
     * @return boolean
     */
    public function isPicked()
    {
        return $this->status == self::STATUS_PICKED;
    }

    /**
     * Mark this line as done.
     *
     * @return void
     */
    public function setDone()
    {
        $this->status = self::STATUS_DONE;
    }

    /**
     * Whether this line is done or not.
     *
     * @return boolean
     */
    public function isDone()
    {
        return $this->status == self::STATUS_DONE;
    }

    /**
     * Whether current line as empty content string or not.
     *
     * @return boolean
     */
    public function isEmpty()
    {
        return $this->input == '' && empty($this->prepend);
    }

    /**
     * Some plugins have a json as insert value, in order to detected such values
     * you can use this method.
     *
     * This will check if the given insert string is a json. In order to deocde the
     * json into a php array afterwards, you can use getArrayInsert();
     *
     * @return boolean Whether current insert string is a json or not.
     */
    public function isJsonInsert()
    {
        return Lexer::isJson($this->input);
    }

    /**
     * Returns the insert json as array.
     *
     * @return array<mixed>
     */
    public function getArrayInsert()
    {
        return Lexer::decodeJson($this->input);
    }

    /**
     * Check whether insert is json/array input. if yes return the requres key.
     *
     * @param string $key The key from the json array
     * @return mixed
     */
    public function insertJsonKey($key)
    {
        if (!$this->isJsonInsert()) {
            return false;
        }

        $insert = $this->getArrayInsert();

        return array_key_exists($key, $insert) ? $insert[$key] : false;
    }

    /**
     * Add debug message for this line if {{Lexer::$debug}} is enabled.
     *
     * @param string $message The message which should be logged.
     * @since 1.3.0
     * @return void
     */
    public function debugInfo($message)
    {
        if ($this->lexer->debug) {
            $this->_debug[] = $message;
        }
    }

    /**
     * Return an array with all debug informations
     *
     * @return array<string>
     * @since 1.3.0
     */
    public function getDebugInfo(): array
    {
        return $this->_debug;
    }
}
