<?php
/*
 * DiffMatchPatch is a port of the google-diff-match-patch (http://code.google.com/p/google-diff-match-patch/)
 * lib to PHP.
 *
 * (c) 2006 Google Inc.
 * (c) 2013 Daniil Skrobov <yetanotherape@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace DiffMatchPatch;

/**
 * Diff offers methods for compute the difference between two texts.
 *
 * @package DiffMatchPatch
 * @author Neil Fraser <fraser@google.com>
 * @author Daniil Skrobov <yetanotherape@gmail.com>
 */
class Diff
{
    /**
     * The data structure representing a diff is an array of arrays:
     * array(
     *      array(Diff::DELETE, "Hello"),
     *      array(Diff::INSERT, "Goodbye"),
     *      array(Diff::EQUAL, " world."),
     * )
     * which means: delete "Hello", add "Goodbye" and keep " world."
     */
    const DELETE = -1;
    const INSERT = 1;
    const EQUAL  = 0;

    /**
     * Number of characters at which line mode speedup is employed.
     */
    const LINEMODE_THRESOLD = 100;

    /**
     * @var float Number of seconds to map a diff before giving up (0 for infinity).
     */
    protected $timeout = 1.0;
    /**
     * @var int Cost of an empty edit operation in terms of edit characters.
     */
    protected $editCost = 4;
    /**
     * @var array
     */
    protected $changes = array();
    /**
     * @var DiffToolkit
     */
    protected $toolkit;

    /**
     * Init object and call main(), if texts passed.
     *
     * @param string|null $text1
     * @param string|null $text2
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($text1 = null, $text2 = null)
    {
        $this->toolkit = new DiffToolkit();

        if (isset($text1, $text2)) {
            $this->main($text1, $text2);
        } elseif (isset($text1) || isset($text2)) {
            throw new \InvalidArgumentException('Only one text parameter passed');
        }
    }

    /**
     * @return float
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * @param $timeout
     *
     * @return $this
     */
    public function setTimeout($timeout)
    {
        $this->timeout = $timeout;

        return $this;
    }

    /**
     * @return int
     */
    public function getEditCost()
    {
        return $this->editCost;
    }

    /**
     * @param int $editCost
     *
     * @return $this
     */
    public function setEditCost($editCost)
    {
        $this->editCost = $editCost;

        return $this;
    }

    /**
     * @return array
     */
    public function getChanges()
    {
        return $this->changes;
    }

    /**
     * @param array $changes
     *
     * @return $this
     */
    public function setChanges($changes)
    {
        $this->changes = $changes;

        return $this;
    }

    /**
     * @return \DiffMatchPatch\DiffToolkit
     */
    public function getToolkit()
    {
        return $this->toolkit;
    }

    /**
     * @param \DiffMatchPatch\DiffToolkit $toolkit
     *
     * @return $this
     */
    public function setToolkit($toolkit)
    {
        $this->toolkit = $toolkit;

        return $this;
    }

    /**
     * Reorder and merge like edit sections.  Merge equalities.
     * Any edit section can move as long as it doesn't cross an equality.
     *
     * @return $this
     */
    public function cleanupMerge()
    {
        $diffs = $this->getChanges();

        $diffs[] = array(
            self::EQUAL,
            '',
        );

        $pointer = 0;
        $count_delete = 0;
        $count_insert = 0;
        $text_delete = '';
        $text_insert = '';
        while ($pointer < count($diffs)) {
            if ($diffs[$pointer][0] == self::INSERT) {
                $count_insert++;
                $text_insert .= $diffs[$pointer][1];
                $pointer++;
            } elseif ($diffs[$pointer][0] == self::DELETE) {
                $count_delete++;
                $text_delete .= $diffs[$pointer][1];
                $pointer++;
            } elseif ($diffs[$pointer][0] == self::EQUAL) {
                // Upon reaching an equality, check for prior redundancies.
                if ($count_delete + $count_insert > 1) {
                    if ($count_delete != 0 && $count_insert != 0) {
                        // Factor out any common prefixies.
                        $commonlength = $this->getToolkit()->commonPrefix($text_insert, $text_delete);
                        if ($commonlength != 0) {
                            $x = $pointer - $count_delete - $count_insert - 1;
                            if ($x >= 0 && $diffs[$x][0] == self::EQUAL) {
                                $diffs[$x][1] .= mb_substr($text_insert, 0, $commonlength);
                            } else {
                                array_unshift($diffs, array(
                                    self::EQUAL,
                                    mb_substr($text_insert, 0, $commonlength),
                                ));
                                $pointer++;
                            }
                            $text_insert = mb_substr($text_insert, $commonlength);
                            $text_delete = mb_substr($text_delete, $commonlength);
                        }
                        // Factor out any common suffixies.
                        $commonlength = $this->getToolkit()->commonSuffix($text_insert, $text_delete);
                        if ($commonlength != 0) {
                            $diffs[$pointer][1] = mb_substr($text_insert, -$commonlength) . $diffs[$pointer][1];
                            $text_insert = mb_substr($text_insert, 0, -$commonlength);
                            $text_delete = mb_substr($text_delete, 0, -$commonlength);
                        }
                    }
                    // Delete the offending records and add the merged ones.
                    if ($count_delete == 0) {
                        array_splice($diffs, $pointer - $count_insert, $count_insert, array(array(
                            self::INSERT,
                            $text_insert,
                        )));
                    } elseif ($count_insert == 0) {
                        array_splice($diffs, $pointer - $count_delete, $count_delete, array(array(
                            self::DELETE,
                            $text_delete,
                        )));
                    } else {
                        array_splice($diffs, $pointer - $count_delete - $count_insert, $count_delete + $count_insert, array(
                            array(
                                self::DELETE,
                                $text_delete,
                            ),
                            array(
                                self::INSERT,
                                $text_insert,
                            ),
                        ));
                    }
                    $pointer = $pointer - $count_delete - $count_insert + 1;
                    if ($count_delete != 0) {
                        $pointer += 1;
                    }
                    if ($count_insert != 0) {
                        $pointer += 1;
                    }
                } elseif ($pointer != 0 && $diffs[$pointer - 1][0] == self::EQUAL) {
                    // Merge this equality with the previous one.
                    $diffs[$pointer - 1][1] .= $diffs[$pointer][1];
                    array_splice($diffs, $pointer, 1);
                } else {
                    $pointer++;
                }
                $count_delete = 0;
                $count_insert = 0;
                $text_delete = '';
                $text_insert = '';
            }
        }

        if ($diffs[count($diffs) - 1][1] == '') {
            array_pop($diffs);
        }

        // Second pass: look for single edits surrounded on both sides by equalities
        // which can be shifted sideways to eliminate an equality.
        // e.g: ABAC -> ABAC
        $changes = false;
        $pointer = 1;
        // Intentionally ignore the first and last element (don't need checking).
        while ($pointer < count($diffs) - 1) {
            if ($diffs[$pointer - 1][0] == self::EQUAL && $diffs[$pointer + 1][0] == self::EQUAL) {
                // This is a single edit surrounded by equalities.
                if (mb_substr($diffs[$pointer][1], -mb_strlen($diffs[$pointer - 1][1])) == $diffs[$pointer - 1][1]) {
                    // Shift the edit over the previous equality.
                    $diffs[$pointer][1] = $diffs[$pointer - 1][1] . mb_substr($diffs[$pointer][1], 0, -mb_strlen($diffs[$pointer - 1][1]));
                    $diffs[$pointer + 1][1] = $diffs[$pointer - 1][1] . $diffs[$pointer + 1][1];
                    array_splice($diffs, $pointer - 1, 1);
                    $changes = true;
                } elseif (mb_substr($diffs[$pointer][1], 0, mb_strlen($diffs[$pointer + 1][1])) == $diffs[$pointer + 1][1]) {
                    // Shift the edit over the next equality.
                    $diffs[$pointer - 1][1] = $diffs[$pointer - 1][1] . $diffs[$pointer + 1][1];
                    $diffs[$pointer][1] = mb_substr($diffs[$pointer][1], mb_strlen($diffs[$pointer + 1][1])) . $diffs[$pointer + 1][1];
                    array_splice($diffs, $pointer + 1, 1);
                    $changes = true;
                }
            }
            $pointer++;
        }
        $this->setChanges($diffs);

        // If shifts were made, the diff needs reordering and another shift sweep.
        if ($changes) {
            $this->cleanupMerge();
        }

        return $this;
    }

    /**
     * Look for single edits surrounded on both sides by equalities
     * which can be shifted sideways to align the edit to a word boundary.
     * e.g: The cat came. -> The cat came.
     *
     * @return $this
     */
    public function cleanupSemanticLossless()
    {
        $diffs = $this->getChanges();

        $pointer = 1;
        // Intentionally ignore the first and last element (don't need checking).
        while ($pointer < count($diffs) - 1) {
            if ($diffs[$pointer - 1][0] == self::EQUAL && $diffs[$pointer + 1][0] == self::EQUAL) {
                // This is a single edit surrounded by equalities.
                $equality1 = $diffs[$pointer - 1][1];
                $edit = $diffs[$pointer][1];
                $equality2 = $diffs[$pointer + 1][1];

                // First, shift the edit as far left as possible.
                $commonOffset = $this->getToolkit()->commonSuffix($equality1, $edit);
                if ($commonOffset) {
                    $commonString = mb_substr($edit, -$commonOffset);
                    $equality1 = mb_substr($equality1, 0, -$commonOffset);
                    $edit = $commonString . mb_substr($edit, 0, -$commonOffset);
                    $equality2 = $commonString . $equality2;
                }

                // Second, step character by character right, looking for the best fit.
                $bestEquality1 = $equality1;
                $bestEdit = $edit;
                $bestEquality2 = $equality2;
                $bestScore = $this->cleanupSemanticScore($equality1, $edit) + $this->cleanupSemanticScore($edit, $equality2);
                while ($edit && $equality2 && mb_substr($edit, 0, 1) == mb_substr($equality2, 0, 1)) {
                    $equality1 .= mb_substr($edit, 0, 1);
                    $edit = mb_substr($edit, 1) . mb_substr($equality2, 0, 1);
                    $equality2 = mb_substr($equality2, 1);
                    $score = $this->cleanupSemanticScore($equality1, $edit) + $this->cleanupSemanticScore($edit, $equality2);
                    // The >= encourages trailing rather than leading whitespace on edits.
                    if ($score >= $bestScore) {
                        $bestScore = $score;
                        $bestEquality1 = $equality1;
                        $bestEdit = $edit;
                        $bestEquality2 = $equality2;
                    }
                }
                if ($diffs[$pointer - 1][1] != $bestEquality1) {
                    // We have an improvement, save it back to the diff.
                    if ($bestEquality1 != '') {
                        $diffs[$pointer - 1][1] = $bestEquality1;
                    } else {
                        array_splice($diffs, $pointer - 1, 1);
                        $pointer -= 1;
                    }
                    $diffs[$pointer][1] = $bestEdit;
                    if ($bestEquality2 != '') {
                        $diffs[$pointer + 1][1] = $bestEquality2;
                    } else {
                        array_splice($diffs, $pointer + 1, 1);
                        $pointer -= 1;
                    }
                }
            }
            $pointer++;
        }
        $this->setChanges($diffs);

        return $this;
    }

    /**
     * Given two strings, compute a score representing whether the internal boundary falls on logical boundaries.
     * Scores range from 6 (best) to 0 (worst).
     *
     * @param string $one First string.
     * @param string $two Second string.
     *
     * @return int The score.
     */
    protected function cleanupSemanticScore($one, $two)
    {
        if ($one == '' || $two == '') {
            // Edges are the best.
            return 6;
        }

        // Each port of this function behaves slightly differently due to
        // subtle differences in each language's definition of things like
        // 'whitespace'.  Since this function's purpose is largely cosmetic,
        // the choice has been made to use each language's native features
        // rather than force total conformity.
        $char1 = mb_substr($one, -1, 1);
        $char2 = mb_substr($two, 0, 1);
        $nonAlphaNumeric1 = preg_match('/[^[:alnum:]]/u', $char1);
        $nonAlphaNumeric2 = preg_match('/[^[:alnum:]]/u', $char2);
        $whitespace1 = $nonAlphaNumeric1 && preg_match('/\s/', $char1);
        $whitespace2 = $nonAlphaNumeric2 && preg_match('/\s/', $char2);
        $lineBreak1 = $whitespace1 && preg_match('/[\r\n]/', $char1);
        $lineBreak2 = $whitespace2 && preg_match('/[\r\n]/', $char2);
        $blankLine1 = $lineBreak1 && preg_match('/\n\r?\n$/', $one);
        $blankLine2 = $lineBreak2 && preg_match('/^\r?\n\r?\n/', $two);

        if ($blankLine1 || $blankLine2) {
            // Five points for blank lines.
            return 5;
        } elseif ($lineBreak1 || $lineBreak2) {
            // Four points for line breaks.
            return 4;
        } elseif ($nonAlphaNumeric1 && !$whitespace1 && $whitespace2) {
            // Three points for end of sentences.
            return 3;
        } elseif ($whitespace1 || $whitespace2) {
            // Two points for whitespace.
            return 2;
        } elseif ($nonAlphaNumeric1 || $nonAlphaNumeric2) {
            // One point for non-alphanumeric.
            return 1;
        }

        return 0;
    }

    /**
     * Reduce the number of edits by eliminating semantically trivial equalities.
     * TODO refactor this cap's code
     *
     * @return $this
     */
    public function cleanupSemantic()
    {
        $diffs = $this->getChanges();

        $changes = false;
        // Stack of indices where equalities are found.
        $equalities = array();
        // Always equal to diffs[equalities[-1]][1]
        $lastequality = null;
        // Index of current position.
        $pointer = 0;
        // Number of chars that changed prior to the equality.
        $length_insertions1 = 0;
        $length_deletions1 = 0;
        // Number of chars that changed after the equality.
        $length_insertions2 = 0;
        $length_deletions2 = 0;

        while ($pointer < count($diffs)) {
            if ($diffs[$pointer][0] == self::EQUAL) {
                $equalities[] = $pointer;
                $length_insertions1 = $length_insertions2;
                $length_insertions2 = 0;
                $length_deletions1 = $length_deletions2;
                $length_deletions2 = 0;
                $lastequality = $diffs[$pointer][1];
            } else {
                if ($diffs[$pointer][0] == self::INSERT) {
                    $length_insertions2 += mb_strlen($diffs[$pointer][1]);
                } else {
                    $length_deletions2 += mb_strlen($diffs[$pointer][1]);
                }
                // Eliminate an equality that is smaller or equal to the edits on both sides of it.
                if (
                    $lastequality != '' &&
                    mb_strlen($lastequality) <= max($length_insertions1, $length_deletions1) &&
                    mb_strlen($lastequality) <= max($length_insertions2, $length_deletions2)
                ) {
                    $insertPointer = array_pop($equalities);
                    // Duplicate record.
                    array_splice($diffs, $insertPointer, 0, array(array(
                       self::DELETE,
                       $lastequality,
                    )));
                    // Change second copy to insert.
                    $diffs[$insertPointer + 1][0] = self::INSERT;
                    // Throw away the previous equality (it needs to be reevaluated).
                    if (count($equalities)) {
                        array_pop($equalities);
                    }
                    if (count($equalities)) {
                        $pointer = end($equalities);
                    } else {
                        $pointer = -1;
                    }
                    // Reset the counters.
                    $length_insertions1 = 0;
                    $length_deletions1 = 0;
                    $length_insertions2 = 0;
                    $length_deletions2 = 0;
                    $lastequality = null;
                    $changes = true;
                }
            }
            $pointer++;
        }
        $this->setChanges($diffs);

        // Normalize the diff.
        if ($changes) {
            $this->cleanupMerge();
        }
        $this->cleanupSemanticLossless();

        $diffs = $this->getChanges();

        // Find any overlaps between deletions and insertions.
        // e.g: abcxxxxxxdef
        //   -> abcxxxdef
        // e.g: xxxabcdefxxx
        //   -> defxxxabc
        // Only extract an overlap if it is as big as the edit ahead or behind it.

        $pointer = 1;
        while ($pointer < count($diffs)) {
            if ($diffs[$pointer - 1][0] == self::DELETE && $diffs[$pointer][0] == self::INSERT) {
                $deletion = $diffs[$pointer - 1][1];
                $insertion = $diffs[$pointer][1];
                $overlap_length1 = $this->getToolkit()->commontOverlap($deletion, $insertion);
                $overlap_length2 = $this->getToolkit()->commontOverlap($insertion, $deletion);

                if ($overlap_length1 >= $overlap_length2) {
                    if ($overlap_length1 >= mb_strlen($deletion) / 2 || $overlap_length1 >= mb_strlen($insertion) / 2) {
                        // Overlap found. Insert an equality and trim the surrounding edits.
                        array_splice($diffs, $pointer, 0, array(array(
                            self::EQUAL,
                            mb_substr($insertion, 0, $overlap_length1),
                        )));
                        $diffs[$pointer - 1][1] = mb_substr($deletion, 0, -$overlap_length1);
                        $diffs[$pointer + 1][1] = mb_substr($insertion, $overlap_length1);
                        $pointer++;
                    }
                } else {
                    if ($overlap_length2 >= mb_strlen($deletion) / 2 || $overlap_length2 >= mb_strlen($insertion) / 2) {
                        // Reverse overlap found.
                        // Insert an equality and swap and trim the surrounding edits.
                        array_splice($diffs, $pointer, 0, array(array(
                            self::EQUAL,
                            mb_substr($deletion, 0, $overlap_length2),
                        )));
                        $diffs[$pointer - 1] = array(
                            self::INSERT,
                            mb_substr($insertion, 0, $overlap_length2),
                        );
                        $diffs[$pointer + 1] = array(
                            self::DELETE,
                            mb_substr($deletion, $overlap_length2),
                        );
                        $pointer++;
                    }
                }
                $pointer++;
            }
            $pointer++;
        }
        $this->setChanges($diffs);

        return $this;
    }

    /**
     * Reduce the number of edits by eliminating operationally trivial equalities.
     * TODO refactor this Cap's code
     *
     * @return $this
     */
    public function cleanupEfficiency() {
        $diffs = $this->getChanges();

        $changes = false;
        // Stack of indices where equalities are found.
        $equalities = array();
        // Always equal to diffs[equalities[-1]][1]
        $lastequality = null;
        // Index of current position.
        $pointer = 0;
        // Is there an insertion operation before the last equality.
        $pre_ins = false;
        // Is there a deletion operation before the last equality.
        $pre_del = false;
        // Is there an insertion operation after the last equality.
        $post_ins = false;
        // Is there a deletion operation after the last equality.
        $post_del = false;

        while ($pointer < count($diffs)) {
            if ($diffs[$pointer][0] == self::EQUAL) {
                if (mb_strlen($diffs[$pointer][1]) < $this->getEditCost() && ($post_ins || $post_del)) {
                    // Candidate found.
                    $equalities[] = $pointer;
                    $pre_ins = $post_ins;
                    $pre_del = $post_del;
                    $lastequality = $diffs[$pointer][1];
                } else {
                    // Not a candidate, and can never become one.
                    $equalities = array();
                    $lastequality = null;
                }
                $post_ins = false;
                $post_del = false;
            } else {
                if ($diffs[$pointer][0] == self::DELETE) {
                    $post_del = true;
                } else {
                    $post_ins = true;
                }

                // Five types to be split:
                // ABXYCD
                // AXCD
                // ABXC
                // AXCD
                // ABXC
                // TODO refactor condition
                if (
                    $lastequality != '' &&
                    (
                        ($pre_ins && $pre_del && $post_ins && $post_del) ||
                        (
                            mb_strlen($lastequality) < $this->getEditCost() / 2 &&
                            ($pre_ins + $pre_del + $post_del + $post_ins == 3)
                        )
                    )
                ) {
                    $insertPointer = array_pop($equalities);
                    // Duplicate record.
                    array_splice($diffs, $insertPointer, 0, array(array(
                        self::DELETE,
                        $lastequality,
                    )));
                    // Change second copy to insert.
                    $diffs[$insertPointer + 1][0] = self::INSERT;
                    // Throw away the previous equality (it needs to be reevaluated).
                    if (count($equalities)) {
                        array_pop($equalities);
                    }
                    $lastequality = null;
                    if ($pre_ins && $pre_del) {
                        // No changes made which could affect previous entry, keep going.
                        $post_ins = true;
                        $post_del = true;
                        $equalities = array();
                    } else {
                        if (count($equalities)) {
                            // Throw away the previous equality.
                            array_pop($equalities);
                        }
                        if (count($equalities)) {
                            $pointer = end($equalities);
                        } else {
                            $pointer = -1;
                        }
                        $post_ins = false;
                        $post_del = false;
                    }
                    $changes = true;
                }
            }
            $pointer++;
        }
        $this->setChanges($diffs);

        if ($changes) {
            $this->cleanupMerge();
        }

        return $this;
    }

    /**
     * Convert a diff array into a pretty HTML report.
     *
     * @return string HTML representation.
     */
    public function prettyHtml()
    {
        $diffs = $this->getChanges();

        $html = '';
        foreach ($diffs as $change) {
            $op = $change[0];
            $data = $change[1];
            $text = str_replace(array(
                '&', '<', '>', "\n",
            ), array(
                '&', '<', '>', '¶
',
            ), $data);

            if ($op == self::INSERT) {
                $html .= '' . $text . '';
            } elseif ($op == self::DELETE) {
                $html .= '' . $text . '';
            } else {
                $html .= '' . $text . '';
            }
        }

        return $html;
    }

    /**
     * Compute and return the source text (all equalities and deletions).
     *
     * @return string Source text.
     */
    public function text1()
    {
        $diffs = $this->getChanges();

        $text = '';
        foreach ($diffs as $change) {
            $op = $change[0];
            $data = $change[1];

            if ($op != self::INSERT) {
                $text .= $data;
            }
        }

        return $text;
    }

    /**
     * Compute and return the destination text (all equalities and insertions).
     *
     * @return string Destination text.
     */
    public function text2()
    {
        $diffs = $this->getChanges();

        $text = '';
        foreach ($diffs as $change) {
            $op = $change[0];
            $data = $change[1];

            if ($op != self::DELETE) {
                $text .= $data;
            }
        }

        return $text;
    }

    /**
     * Crush the diff into an encoded string which describes the operations
     * required to transform text1 into text2.
     * E.g. =3\t-2\t+ing  -> Keep 3 chars, delete 2 chars, insert 'ing'.
     * Operations are tab-separated.  Inserted text is escaped using %xx notation.
     *
     * @return string Delta text.
     */
    public function toDelta() {
        $diffs = $this->getChanges();

        $text = array();
        foreach ($diffs as $change) {
            $op = $change[0];
            $data = $change[1];

            if ($op == self::INSERT) {
                $text[] = '+'. Utils::escapeString($data);
            } elseif ($op == self::DELETE) {
                $text[] = '-' . mb_strlen($data);
            } else {
                $text[] = '=' . mb_strlen($data);
            }
        }

        return implode("\t", $text);
    }

    /**
     * Given the original text1, and an encoded string which describes the
     * operations required to transform text1 into text2, compute the full diff.
     *
     * @param string $text1 Source string for the diff.
     * @param string $delta Delta text.
     *
     * @throws \InvalidArgumentException If invalid input. TODO create exception class
     * @return $this.
     */
    public function fromDelta($text1, $delta)
    {
        $diffs = array();
        // Cursor in text1
        $pointer = 0;
        $tokens = explode("\t", $delta);
        foreach ($tokens as $token) {
            if ($token == '') {
                // Blank tokens are ok (from a trailing \t).
                continue;
            }
            // Each token begins with a one character parameter which specifies the
            // operation of this token (delete, insert, equality).
            $op = mb_substr($token, 0, 1);
            $param = mb_substr($token, 1);
            switch ($op) {
                case '+':
                    $diffs[] = array(
                        self::INSERT,
                        Utils::unescapeString($param),
                    );
                    break;
                case '-':
                case '=':
                    if (!is_numeric($param)) {
                        throw new \InvalidArgumentException('Invalid number in delta: ' . $param);
                    } elseif ($param < 0) {
                        throw new \InvalidArgumentException('Negative number in delta: ' . $param);
                    } else {
                        $n = (int) $param;
                    }
                    $text = mb_substr($text1, $pointer, $n);
                    $pointer += $n;
                    $diffs[] = array(
                        $op == '=' ? self::EQUAL : self::DELETE,
                        $text,
                    );
                    break;
                default:
                    // Anything else is an error.
                    throw new \InvalidArgumentException('Invalid diff operation in delta: ' . $op);
            }
        }
        if ($pointer != mb_strlen($text1)) {
            throw new \InvalidArgumentException('Delta length (' . $pointer . ') does not equal source text length (' . mb_strlen($text1) . ').');
        }
        $this->setChanges($diffs);

        return $this;
    }

    /**
     * Compute and return location in text2 equivalent to the $loc in text1.
     * e.g. "The cat" vs "The big cat", 1->1, 5->8
     *
     * @param int $loc Location within text1.
     *
     * @return int Location within text2.
     */
    public function xIndex($loc)
    {
        $diffs = $this->getChanges();

        $chars1 = 0;
        $chars2 = 0;
        $last_chars1 = 0;
        $last_chars2 = 0;

        $i = 0;
        foreach ($diffs as $change) {
            $op = $change[0];
            $text = $change[1];
            // Equality or deletion.
            if ($op != self::INSERT) {
                $chars1 += mb_strlen($text);
            }
            // Equality or insertion.
            if ($op != self::DELETE) {
                $chars2 += mb_strlen($text);
            }
            // Overshot the location.
            if ($chars1 > $loc) {
                break;
            }
            $last_chars1 = $chars1;
            $last_chars2 = $chars2;
            $i++;
        }

        // The location was deleted.
        if (count($diffs) != $i && $diffs[$i][0] == self::DELETE) {
            return $last_chars2;
        }
        return $loc + $last_chars2 - $last_chars1;
    }

    /**
     * Compute the Levenshtein distance; the number of inserted, deleted or substituted characters.
     *
     * @return int Number of changes.
     */
    public function levenshtein()
    {
        $diffs = $this->getChanges();

        $levenshtein = 0;
        $insertions = 0;
        $deletions = 0;

        foreach ($diffs as $change) {
            $op = $change[0];
            $text = $change[1];

            switch ($op) {
                case self::INSERT:
                    $insertions += mb_strlen($text);
                    break;
                case self::DELETE:
                    $deletions += mb_strlen($text);
                    break;
                case self::EQUAL:
                    // A deletion and an insertion is one substitution.
                    $levenshtein += max($insertions, $deletions);
                    $insertions = 0;
                    $deletions = 0;
                    break;
            }
        }
        $levenshtein += max($insertions, $deletions);

        return $levenshtein;
    }

    /**
     * Find the differences between two texts.  Simplifies the problem by
     * stripping any common prefix or suffix off the texts before diffing.
     *
     * @param string $text1      Old string to be diffed.
     * @param string $text2      New string to be diffed.
     * @param bool   $checklines Optional speedup flag.  If present and false, then don't run
     *                           a line-level diff first to identify the changed areas.
     *                           Defaults to true, which does a faster, slightly less optimal diff.
     * @param int    $deadline   Optional time when the diff should be complete by.  Used internally for recursive calls.
     *                           Users should set $this->timeout instead.
     *
     * @throws \InvalidArgumentException If texts is null. TODO create exception class
     * @return self
     */
    public function main($text1, $text2, $checklines = true, $deadline = null)
    {
        // Set a deadline by which time the diff must be complete.
        if (!isset($deadline)) {
            if ($this->getTimeout() <= 0) {
                $deadline  = PHP_INT_MAX;
            } else {
                $deadline = microtime(1) + $this->getTimeout();
            }
        }

        if (!isset($text1, $text2)) {
            throw new \InvalidArgumentException();
        }

        // Check for equality (speedup).
        if ($text1 == $text2) {
            if ($text1 != '') {
                $this->setChanges(array(
                    array(self::EQUAL, $text1),
                ));
                return $this;
            }
            return $this;
        }

        $prevInternalEncoding = mb_internal_encoding();
        $newInternalEncoding = 'UCS-2LE';
        if ($prevInternalEncoding != $newInternalEncoding) {
            mb_internal_encoding($newInternalEncoding);

            $errorReportingLevel = error_reporting();
            error_reporting($errorReportingLevel & ~E_NOTICE);

            $text1Draft = iconv($prevInternalEncoding, $newInternalEncoding, $text1);
            $text2Draft = iconv($prevInternalEncoding, $newInternalEncoding, $text2);

            if ($text1Draft === false || $text2Draft === false) {
                $newInternalEncoding = 'UCS-4LE';

                $text1Draft = iconv($prevInternalEncoding, $newInternalEncoding, $text1);
                $text2Draft = iconv($prevInternalEncoding, $newInternalEncoding, $text2);
            }

            $text1 = $text1Draft;
            $text2 = $text2Draft;

            error_reporting($errorReportingLevel);
        }

        // Trim off common prefix (speedup).
        $commonLength = $this->getToolkit()->commonPrefix($text1, $text2);
        if ($commonLength == 0) {
            $commonPrefix = '';
        } else {
            $commonPrefix = mb_substr($text1, 0, $commonLength);
            $text1 = mb_substr($text1, $commonLength);
            $text2 = mb_substr($text2, $commonLength);
        }

        // Trim off common suffix (speedup).
        $commonLength = $this->getToolkit()->commonSuffix($text1, $text2);
        if ($commonLength == 0) {
            $commonSuffix = '';
        } else {
            $commonSuffix = mb_substr($text1, -$commonLength);
            $text1 = mb_substr($text1, 0, -$commonLength);
            $text2 = mb_substr($text2, 0, -$commonLength);
        }

        // Compute the diff on the middle block.
        $diffs = $this->compute($text1, $text2, $checklines, $deadline);

        // Restore the prefix and suffix.
        if ($commonPrefix != '') {
            array_unshift($diffs, array(self::EQUAL, $commonPrefix));
        }
        if ($commonSuffix != '') {
            array_push($diffs, array(self::EQUAL, $commonSuffix));
        }

        if ($newInternalEncoding != $prevInternalEncoding) {
            mb_internal_encoding($prevInternalEncoding);
            foreach ($diffs as &$change) {
                $change[1] = iconv($newInternalEncoding, $prevInternalEncoding, $change[1]);
            }
            unset($change);
        }

        $this->setChanges($diffs);

        $this->cleanupMerge();

        return $this;
    }

    /**
     * Find the differences between two texts.  Assumes that the texts do not
     * have any common prefix or suffix.
     *
     * @param string $text1         Old string to be diffed.
     * @param string $text2         New string to be diffed.
     * @param bool   $checklines    Speedup flag.  If false, then don't run a line-level diff
     *                              first to identify the changed areas.
     *                              If true, then run a faster, slightly less optimal diff.
     * @param int    $deadline      Time when the diff should be complete by.
     *
     * @return array Array of changes.
     */
    protected function compute($text1, $text2, $checklines, $deadline)
    {
        if ($text1 == '') {
            // Just add some text (speedup).
            return array(
                array(self::INSERT, $text2),
            );
        }

        if ($text2 == '') {
            // Just delete some text (speedup).
            return array(
                array(self::DELETE, $text1),
            );
        }

        if (mb_strlen($text1) < mb_strlen($text2)) {
            $shortText = $text1;
            $longText = $text2;
        } else {
            $shortText = $text2;
            $longText = $text1;
        }

        $i = mb_strpos($longText, $shortText);
        if ($i !== false) {
            // Shorter text is inside the longer text (speedup).
            $diffs = array(
                array(self::INSERT, mb_substr($longText, 0, $i)),
                array(self::EQUAL, $shortText),
                array(self::INSERT, mb_substr($longText, $i + mb_strlen($shortText))),
            );
            // Swap insertions for deletions if diff is reversed.
            if (mb_strlen($text2) < mb_strlen($text1)) {
                $diffs[0][0] = self::DELETE;
                $diffs[2][0] = self::DELETE;
            }
            return $diffs;
        }

        if (mb_strlen($shortText) == 1) {
            // Single character string.
            // After the previous speedup, the character can't be an equality.
            $diffs = array(
                array(self::DELETE, $text1),
                array(self::INSERT, $text2),
            );
            return $diffs;
        }

        // Don't risk returning a non-optimal diff if we have unlimited time.
        if ($this->getTimeout() > 0) {
            // Check to see if the problem can be split in two.
            $hm = $this->getToolkit()->halfMatch($text1, $text2);
            if ($hm) {
                // A half-match was found, sort out the return data.
                list($text1_a, $text1_b, $text2_a, $text2_b, $mid_common) = $hm;
                // Send both pairs off for separate processing.
                $diffA = new Diff();
                $diffA->main($text1_a, $text2_a, $checklines, $deadline);
                $diffB = new Diff();
                $diffB->main($text1_b, $text2_b, $checklines, $deadline);
                // Merge the results.
                $diffs = array_merge(
                    $diffA->getChanges(),
                    array(
                        array(self::EQUAL, $mid_common),
                    ),
                    $diffB->getChanges()
                );
                return $diffs;
            }
        }

        if ($checklines
          && mb_strlen($text1) > Diff::LINEMODE_THRESOLD
          && mb_strlen($text2) > Diff::LINEMODE_THRESOLD) {
            return $this->lineMode($text1, $text2, $deadline);
        }

        return $this->bisect($text1, $text2, $deadline);
    }

    /**
     * Do a quick line-level diff on both strings, then rediff the parts for greater accuracy.
     * This speedup can produce non-minimal diffs.
     *
     * @param string $text1    Old string to be diffed.
     * @param string $text2    New string to be diffed.
     * @param int    $deadline Time when the diff should be complete by.
     *
     * @return array Array of changes.
     */
    protected function lineMode($text1, $text2, $deadline)
    {
        // Scan the text on a line-by-line basis first.
        list($text1, $text2, $lineArray) = $this->getToolkit()->linesToChars($text1, $text2);

        $diff = new Diff();
        $diff->main($text1, $text2, false, $deadline);
        $diffs = $diff->getChanges();

        // Convert the diff back to original text.
        $this->getToolkit()->charsToLines($diffs, $lineArray);
        $diff->setChanges($diffs);

        // Eliminate freak matches (e.g. blank lines)
        $diff->cleanupSemantic();
        $diffs = $diff->getChanges();

        // Rediff any replacement blocks, this time character-by-character.
        // Add a dummy entry at the end.
        array_push($diffs, array(self::EQUAL, ''));
        $pointer = 0;
        $countDelete = 0;
        $countInsert = 0;
        $textDelete = '';
        $textInsert = '';

        while ($pointer < count($diffs)) {
            switch ($diffs[$pointer][0]) {
                case self::DELETE:
                    $countDelete++;
                    $textDelete .= $diffs[$pointer][1];
                    break;
                case self::INSERT:
                $countInsert++;
                    $textInsert .= $diffs[$pointer][1];
                    break;
                case self::EQUAL:
                    // Upon reaching an equality, check for prior redundancies.
                    if ($countDelete > 0 && $countInsert > 0) {
                        // Delete the offending records and add the merged ones.
                        $subDiff = new Diff();
                        $subDiff->main($textDelete, $textInsert, false, $deadline);
                        array_splice($diffs, $pointer - $countDelete - $countInsert, $countDelete + $countInsert, $subDiff->getChanges());
                        $pointer = $pointer - $countDelete - $countInsert + count($subDiff->getChanges());
                    }
                    $countDelete = 0;
                    $countInsert = 0;
                    $textDelete = '';
                    $textInsert = '';
                    break;
            }
            $pointer++;
        }

        // Remove the dummy entry at the end.
        array_pop($diffs);

        return $diffs;
    }

    /**
     * Find the 'middle snake' of a diff, split the problem in two
     * and return the recursively constructed diff.
     * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
     *
     * @param string $text1    Old string to be diffed.
     * @param string $text2    New string to be diffed.
     * @param int    $deadline Time at which to bail if not yet complete.
     *
     * @return array Array of diff arrays.
     */
    protected function bisect($text1, $text2, $deadline)
    {
        // Cache the text lengths to prevent multiple calls.
        $text1Length = mb_strlen($text1);
        $text2Length = mb_strlen($text2);
        $maxD = (int)(($text1Length + $text2Length + 1) / 2);
        $vOffset = $maxD;
        $vLength = 2 * $maxD;
        $v1 = array_fill(0, $vLength, -1);
        $v1[$vOffset + 1] = 0;
        $v2 = $v1;
        $delta = $text1Length - $text2Length;

        // If the total number of characters is odd, then the front path will collide with the reverse path.
        $front = $delta % 2 != 0;

        // Offsets for start and end of k loop.
        // Prevents mapping of space beyond the grid.
        $k1Start = 0;
        $k1End = 0;
        $k2Start = 0;
        $k2End = 0;

        for ($d = 0; $d < $maxD; $d++) {
            // Bail out if deadline is reached.
            if (microtime(1) > $deadline) {
                break;
            }

            // Walk the front path one step.
            for ($k1 = -$d + $k1Start; $k1 < $d + 1 - $k1End; $k1 += 2) {
                $k1Offset = $vOffset + $k1;
                if ($k1 == -$d || ($k1 != $d && $v1[$k1Offset - 1] < $v1[$k1Offset + 1])) {
                    $x1 = $v1[$k1Offset + 1];
                } else {
                    $x1 = $v1[$k1Offset - 1] + 1;
                }
                $y1 = $x1 - $k1;
                while ($x1 < $text1Length && $y1 < $text2Length && mb_substr($text1, $x1, 1) == mb_substr($text2, $y1, 1)) {
                    $x1++;
                    $y1++;
                }
                $v1[$k1Offset] = $x1;
                if ($x1 > $text1Length) {
                    // Ran off the right of the graph.
                    $k1End += 2;
                } elseif ($y1 > $text2Length) {
                    // Ran off the bottom of the graph.
                    $k1Start += 2;
                } elseif ($front) {
                    $k2Offset = $vOffset + $delta - $k1;
                    if ($k2Offset >= 0 && $k2Offset < $vLength && $v2[$k2Offset] != -1) {
                        // Mirror x2 onto top-left coordinate system.
                        $x2 = $text1Length - $v2[$k2Offset];
                        if ($x1 >= $x2) {
                            // Overlap detected.
                            return $this->bisectSplit($text1, $text2, $x1, $y1, $deadline);
                        }
                    }
                }
            }

            // Walk the reverse path one step.
            for ($k2 = -$d + $k2Start; $k2 < $d + 1 - $k2End; $k2 += 2) {
                $k2Offset = $vOffset + $k2;
                if ($k2 == -$d || ($k2 != $d && $v2[$k2Offset - 1] < $v2[$k2Offset + 1])) {
                    $x2 = $v2[$k2Offset + 1];
                } else {
                    $x2 = $v2[$k2Offset - 1] + 1;
                }
                $y2 = $x2 - $k2;
                while ($x2 < $text1Length && $y2 < $text2Length && mb_substr($text1, -$x2 - 1, 1) == mb_substr($text2, -$y2 - 1, 1)) {
                    $x2++;
                    $y2++;
                }
                $v2[$k2Offset] = $x2;
                if ($x2 > $text1Length) {
                    // Ran off the right of the graph.
                    $k2End += 2;
                } elseif ($y2 > $text2Length) {
                    // Ran off the bottom of the graph.
                    $k2Start += 2;
                } elseif (!$front) {
                    $k1Offset = $vOffset + $delta - $k2;
                    if ($k1Offset >= 0 && $k1Offset < $vLength && $v1[$k1Offset] != -1) {
                        $x1 = $v1[$k1Offset];
                        $y1 = $vOffset + $x1 - $k1Offset;
                        // Mirror x2 onto top-left coordinate system.
                        $x2 = $text1Length - $x2;
                        if ($x1 >= $x2) {
                            // Overlap detected.
                            return $this->bisectSplit($text1, $text2, $x1, $y1, $deadline);
                        }
                    }
                }
            }
        }
        // Diff took too long and hit the deadline or
        // number of diffs equals number of characters, no commonality at all.
        return array(
          array(self::DELETE, $text1),
          array(self::INSERT, $text2),
        );
    }

    /**
     * Given the location of the 'middle snake', split the diff in two parts and recurse.
     *
     * @param string $text1    Old string to be diffed.
     * @param string $text2    New string to be diffed.
     * @param int    $x        Index of split point in text1.
     * @param int    $y        Index of split point in text2.
     * @param int    $deadline Time at which to bail if not yet complete.
     *
     * @return array Array of diff arrays.
     */
    protected function bisectSplit($text1, $text2, $x, $y, $deadline)
    {
        $text1A = mb_substr($text1, 0, $x);
        $text2A = mb_substr($text2, 0, $y);
        $text1B = mb_substr($text1, $x);
        $text2B = mb_substr($text2, $y);

        // Compute both diffs serially.
        $diffA = new Diff();
        $diffA->main($text1A, $text2A, false, $deadline);
        $diffB = new Diff();
        $diffB->main($text1B, $text2B, false, $deadline);

        return array_merge($diffA->getChanges(), $diffB->getChanges());
    }
}


<?php
/*
 * DiffMatchPatch is a port of the google-diff-match-patch (http://code.google.com/p/google-diff-match-patch/)
 * lib to PHP.
 *
 * (c) 2006 Google Inc.
 * (c) 2013 Daniil Skrobov <yetanotherape@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace DiffMatchPatch;

/**
 * This toolkit contains functions for working with texts.
 *
 * @package DiffMatchPatch
 * @author Neil Fraser <fraser@google.com>
 * @author Daniil Skrobov <yetanotherape@gmail.com>
 */
class DiffToolkit {
    /**
     * Determine the common prefix of two strings.
     *
     * @param string $text1 First string.
     * @param string $text2 Second string.
     *
     * @return int The number of characters common to the start of each string.
     */
    public function commonPrefix($text1, $text2)
    {
        // Quick check for common null cases.
        if ($text1 == '' || $text2 == '' || mb_substr($text1, 0, 1) != mb_substr($text2, 0, 1)) {
            return 0;
        }
        // Binary search.
        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
        $pointermin = 0;
        $pointermax = min(mb_strlen($text1), mb_strlen($text2));
        $pointermid = $pointermax;
        $pointerstart = 0;
        while ($pointermin < $pointermid) {
            if (mb_substr($text1, $pointerstart, $pointermid - $pointerstart) == mb_substr($text2, $pointerstart,
                    $pointermid - $pointerstart)
            ) {
                $pointermin = $pointermid;
                $pointerstart = $pointermin;
            } else {
                $pointermax = $pointermid;
            }
            $pointermid = (int)(($pointermax - $pointermin) / 2) + $pointermin;
        }

        return $pointermid;
    }

    /**
     * Determine the common suffix of two strings.
     *
     * @param string $text1 First string.
     * @param string $text2 Second string.
     *
     * @return int The number of characters common to the end of each string.
     */
    public function commonSuffix($text1, $text2)
    {
        // Quick check for common null cases.
        if ($text1 == '' || $text2 == '' || mb_substr($text1, -1, 1) != mb_substr($text2, -1, 1)) {
            return 0;
        }
        // Binary search.
        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
        $pointermin = 0;
        $pointermax = min(mb_strlen($text1), mb_strlen($text2));
        $pointermid = $pointermax;
        $pointerend = 0;
        while ($pointermin < $pointermid) {
            if (mb_substr($text1, -$pointermid, $pointermid - $pointerend) == mb_substr($text2, -$pointermid,
                    $pointermid - $pointerend)
            ) {
                $pointermin = $pointermid;
                $pointerend = $pointermin;
            } else {
                $pointermax = $pointermid;
            }
            $pointermid = (int)(($pointermax - $pointermin) / 2) + $pointermin;
        }

        return $pointermid;
    }

    /**
     * Determine if the suffix of one string is the prefix of another.
     *
     * @param string $text1 First string.
     * @param string $text2 Second string.
     *
     * @return int The number of characters common to the end of the first string and the start of the second string.
     */
    public function commontOverlap($text1, $text2)
    {
        // Cache the text lengths to prevent multiple calls.
        $text1_length = mb_strlen($text1);
        $text2_length = mb_strlen($text2);

        // Eliminate the null case.
        if (!$text1_length || !$text2_length) {
            return 0;
        }

        // Truncate the longer string.
        if ($text1_length > $text2_length) {
            $text1 = mb_substr($text1, -$text2_length);
        } elseif ($text1_length < $text2_length) {
            $text2 = mb_substr($text2, 0, $text1_length);
        }
        $text_length = min($text1_length, $text2_length);

        // Quick check for the worst case.
        if ($text1 == $text2) {
            return $text_length;
        }

        // Start by looking for a single character match
        // and increase length until no match is found.
        // Performance analysis: http://neil.fraser.name/news/2010/11/04/
        $best = 0;
        $length = 1;
        while (true) {
            $pattern = mb_substr($text1, -$length);
            $found = mb_strpos($text2, $pattern);
            if ($found === false) {
                break;
            }
            $length += $found;
            if ($found == 0 || mb_substr($text1, -$length) == mb_substr($text2, 0, $length)) {
                $best = $length;
                $length += 1;
            }
        }

        return $best;
    }

    /**
     * Do the two texts share a substring which is at least half the length of the longer text?
     * This speedup can produce non-minimal diffs.
     *
     * @param string $text1 First string.
     * @param string $text2 Second string.
     *
     * @return null|array Five element array, containing the prefix of text1, the suffix of text1,
     * the prefix of text2, the suffix of text2 and the common middle.  Or null if there was no match.
     */
    public function halfMatch($text1, $text2)
    {
        if (mb_strlen($text1) > mb_strlen($text2)) {
            $longtext = $text1;
            $shorttext = $text2;
        } else {
            $shorttext = $text1;
            $longtext = $text2;
        }

        if (mb_strlen($longtext) < 4 || mb_strlen($shorttext) * 2 < mb_strlen(mb_strlen($longtext))) {
            // Pointless
            return null;
        }

        // First check if the second quarter is the seed for a half-match.
        $hm1 = $this->halfMatchI($longtext, $shorttext, (int)((mb_strlen($longtext) + 3) / 4));
        // Check again based on the third quarter.
        $hm2 = $this->halfMatchI($longtext, $shorttext, (int)((mb_strlen($longtext) + 1) / 2));

        if (empty($hm1) && empty($hm2)) {
            return null;
        } elseif (empty($hm2)) {
            $hm = $hm1;
        } elseif (empty($hm1)) {
            $hm = $hm2;
        } else {
            // Both matched.  Select the longest.
            if (mb_strlen($hm1[4] > $hm2[4])) {
                $hm = $hm1;
            } else {
                $hm = $hm2;
            }
        }

        // A half-match was found, sort out the return data.
        if (mb_strlen($text1) > mb_strlen($text2)) {
            return array($hm[0], $hm[1], $hm[2], $hm[3], $hm[4]);
        } else {
            return array($hm[2], $hm[3], $hm[0], $hm[1], $hm[4]);
        }
    }

    /**
     * Does a substring of shorttext exist within longtext such that the substring
     * is at least half the length of longtext?
     *
     * @param string $longtext  Longer string.
     * @param string $shorttext Shorter string.
     * @param int    $i         Start index of quarter length substring within longtext.
     *
     * @return null|array Five element array, containing the prefix of longtext, the suffix of longtext,
     * the prefix of shorttext, the suffix of shorttext and the common middle.  Or null if there was no match.
     */
    protected function halfMatchI($longtext, $shorttext, $i)
    {
        $seed = mb_substr($longtext, $i, (int)(mb_strlen($longtext) / 4));
        $best_common = $best_longtext_a = $best_longtext_b = $best_shorttext_a = $best_shorttext_b = '';

        $j = mb_strpos($shorttext, $seed);
        while ($j !== false) {
            $prefixLegth = $this->commonPrefix(mb_substr($longtext, $i), mb_substr($shorttext, $j));
            $suffixLegth = $this->commonSuffix(mb_substr($longtext, 0, $i), mb_substr($shorttext, 0, $j));
            if (mb_strlen($best_common) < $suffixLegth + $prefixLegth) {
                $best_common = mb_substr($shorttext, $j - $suffixLegth, $suffixLegth) . mb_substr($shorttext, $j,
                        $prefixLegth);
                $best_longtext_a = mb_substr($longtext, 0, $i - $suffixLegth);
                $best_longtext_b = mb_substr($longtext, $i + $prefixLegth);
                $best_shorttext_a = mb_substr($shorttext, 0, $j - $suffixLegth);
                $best_shorttext_b = mb_substr($shorttext, $j + $prefixLegth);
            }
            $j = mb_strpos($shorttext, $seed, $j + 1);
        }

        if (mb_strlen($best_common) * 2 >= mb_strlen($longtext)) {
            return array($best_longtext_a, $best_longtext_b, $best_shorttext_a, $best_shorttext_b, $best_common);
        } else {
            return null;
        }
    }

    /**
     * Split two texts into an array of strings.  Reduce the texts to a string of hashes where each
     * Unicode character represents one line.
     *
     * @param string $text1 First string.
     * @param string $text2 Second string.
     *
     * @return array Three element array, containing the encoded text1, the encoded text2 and the array
     * of unique strings.  The zeroth element of the array of unique strings is intentionally blank.
     */
    public function linesToChars($text1, $text2)
    {
        // e.g. $lineArray[4] == "Hello\n"
        $lineArray = array();
        // e.g. $lineHash["Hello\n"] == 4
        $lineHash = array();

        // "\x00" is a valid character, but various debuggers don't like it.
        // So we'll insert a junk entry to avoid generating a null character.
        $lineArray[] = '';

        $chars1 = $this->linesToCharsMunge($text1, $lineArray, $lineHash);
        $chars2 = $this->linesToCharsMunge($text2, $lineArray, $lineHash);

        return array($chars1, $chars2, $lineArray);
    }

    /**
     * Split a text into an array of strings.  Reduce the texts to a string of hashes where each
     * Unicode character represents one line.
     * Modifies $lineArray and $lineHash. TODO try to fix it!
     *
     * @param string $text String to encode.
     * @param array  $lineArray
     * @param array  $lineHash
     *
     * @return string Encoded string.
     */
    protected function linesToCharsMunge($text, array &$lineArray, array &$lineHash)
    {
        // Simple string concat is even faster than implode() in PHP.
        $chars = '';

        $delimiter = iconv('UTF-8', mb_internal_encoding(), "\n");

        // TODO optimize code
        // explode('\n', $text) would temporarily double our memory footprint,
        // but mb_strpos() and mb_substr() work too slow
        $lines = explode($delimiter, $text);

        $last_line_has_delimiter = end($lines) === '';
        if ($last_line_has_delimiter) {
            array_pop($lines);
        }

        foreach ($lines as $i => $line) {
            if (isset($lines[$i + 1]) || $last_line_has_delimiter) {
                $line .= $delimiter;
            }
            if (!isset($lineHash[$line])) {
                $lineArray[] = $line;
                $lineHash[$line] = count($lineArray) - 1;
            }
            $chars .= Utils::unicodeChr($lineHash[$line]);
        }

        return $chars;
    }

    /**
     * Rehydrate the text in a diff from a string of line hashes to real lines of text.
     * Modifies $diffs. TODO try to fix it!
     *
     * @param array $diffs Array of diff arrays.
     * @param array $lineArray Array of unique strings.
     */
    public function charsToLines(&$diffs, $lineArray)
    {
        foreach ($diffs as &$diff) {
            $text = '';
            for ($i = 0; $i < mb_strlen($diff[1]); $i++) {
                $char = mb_substr($diff[1], $i, 1);
                $text .= $lineArray[Utils::unicodeOrd($char)];
            }
            $diff[1] = $text;
        }
        unset($diff);
    }
}


<?php
/*
 * DiffMatchPatch is a port of the google-diff-match-patch (http://code.google.com/p/google-diff-match-patch/)
 * lib to PHP.
 *
 * (c) 2006 Google Inc.
 * (c) 2013 Daniil Skrobov <yetanotherape@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace DiffMatchPatch;

/**
 * Functions for diff, match and patch.
 *
 * Computes the difference between two texts to create a patch. Applies the patch onto another text,
 * allowing for errors.
 * This class implements the same API as all other google-diff-match-patch libs. It was created for
 * compatibility reason only.
 *
 * @property float Diff_Timeout          Number of seconds to map a diff before giving up (0 for infinity).
 * @property int   Diff_EditCost         Cost of an empty edit operation in terms of edit characters.
 * @property float Match_Threshold       At what point is no match declared (0.0 = perfection, 1.0 = very loose).
 * @property int   Match_Distance        How far to search for a match (0 = exact location, 1000+ = broad match).
 *                                       A match this many characters away from the expected location will add
 *                                       1.0 to the score (0.0 is a perfect match).
 * @property int   Match_MaxBits         The number of bits in an int.
 * @property float Patch_DeleteThreshold When deleting a large block of text (over ~64 characters), how close do
 *                                       the contents have to be to match the expected contents. (0.0 = perfection,
 *                                       1.0 = very loose).  Note that Match_Threshold controls how closely the
 *                                       end points of a delete need to match.
 * @property int   Patch_Margin          Chunk size for context length.
 *
 * @package DiffMatchPatch
 * @author Neil Fraser <fraser@google.com>
 * @author Daniil Skrobov <yetanotherape@gmail.com>
 */
class DiffMatchPatch
{
    /**
     * The data structure representing a diff is an array of arrays:
     * array(
     *      array(DiffMatchPatch::DIFF_DELETE, "Hello"),
     *      array(DiffMatchPatch::DIFF_INSERT, "Goodbye"),
     *      array(DiffMatchPatch::DIFF_EQUAL, " world."),
     * )
     * which means: delete "Hello", add "Goodbye" and keep " world."
     */
    const DIFF_DELETE = Diff::DELETE;
    const DIFF_INSERT = Diff::INSERT;
    const DIFF_EQUAL  = Diff::EQUAL;

    /**
     * @var Diff
     */
    protected $diff;
    /**
     * @var Match
     */
    protected $match;
    /**
     * @var Patch
     */
    protected $patch;

    /**
     * Proxy getting properties to real objects.
     *
     * @param string $name Property name.
     *
     * @throws \UnexpectedValueException If property unknown.
     * @return float
     */
    public function __get($name)
    {
        switch ($name){
            case 'Diff_Timeout':
                $result = $this->diff->getTimeout();
                break;
            case 'Diff_EditCost':
                $result = $this->diff->getEditCost();
                break;
            case 'Match_Threshold':
                $result = $this->match->getThreshold();
                break;
            case 'Match_Distance':
                $result = $this->match->getDistance();
                break;
            case 'Match_MaxBits':
                $result = $this->match->getMaxBits();
                break;
            case 'Patch_DeleteThreshold':
                $result = $this->patch->getDeleteTreshold();
                break;
            case 'Patch_Margin':
                $result = $this->patch->getMargin();
                break;
            default:
                throw new \UnexpectedValueException('Unknown property: ' . $name);
        }

        return $result;
    }

    /**
     * Proxy setting properties to real objects.
     *
     * @param string $name  Property name.
     * @param mixed  $value Property value.
     *
     * @throws \UnexpectedValueException If property unknown.
     * @return float
     */
    public function __set($name, $value)
    {
        switch ($name){
            case 'Diff_Timeout':
                $this->diff->setTimeout($value);
                break;
            case 'Diff_EditCost':
                $this->diff->setEditCost($value);
                break;
            case 'Match_Threshold':
                $this->match->setThreshold($value);
                break;
            case 'Match_Distance':
                $this->match->setDistance($value);
                break;
            case 'Match_MaxBits':
                $this->match->setMaxBits($value);
                break;
            case 'Patch_DeleteThreshold':
                $this->patch->setDeleteTreshold($value);
                break;
            case 'Patch_Margin':
                $this->patch->setMargin($value);
                break;
            default:
                throw new \UnexpectedValueException('Unknown property: ' . $name);
        }
    }

    public function __construct()
    {
        $this->diff = new Diff();
        $this->match = new Match();
        $this->patch = new Patch($this->diff, $this->match);
    }

    /**
     * Find the differences between two texts.  Simplifies the problem by
     * stripping any common prefix or suffix off the texts before diffing.
     *
     * @param string $text1      Old string to be diffed.
     * @param string $text2      New string to be diffed.
     * @param bool   $checklines Optional speedup flag.  If present and false, then don't run
     *                           a line-level diff first to identify the changed areas.
     *                           Defaults to true, which does a faster, slightly less optimal diff.
     *
     * @throws \InvalidArgumentException If texts is null.
     * @return array Array of changes.
     */
    public function diff_main($text1, $text2, $checklines = true)
    {
        return $this->diff->main($text1, $text2, $checklines)->getChanges();
    }

    /**
     * Reduce the number of edits by eliminating semantically trivial equalities.
     * Modifies $diffs.
     *
     * @param array $diffs Array of diff arrays.
     */
    public function diff_cleanupSemantic(&$diffs)
    {
        $this->diff->setChanges($diffs);
        $this->diff->cleanupSemantic();
        $diffs = $this->diff->getChanges();
    }

    /**
     * Reduce the number of edits by eliminating operationally trivial equalities.
     * Modifies $diffs.
     *
     * @param array $diffs Array of diff arrays.
     */
    public function diff_cleanupEfficiency(&$diffs)
    {
        $this->diff->setChanges($diffs);
        $this->diff->cleanupEfficiency();
        $diffs = $this->diff->getChanges();
    }

    /**
     * Compute the Levenshtein distance; the number of inserted, deleted or substituted characters.
     *
     * @param array $diffs Array of diff arrays.
     *
     * @return int Number of changes.
     */
    public function diff_levenshtein($diffs)
    {
        $this->diff->setChanges($diffs);
        return $this->diff->levenshtein();
    }

    /**
     * Convert a diff array into a pretty HTML report.
     *
     * @param array $diffs Array of diff arrays.
     *
     * @return string HTML representation.
     */
    public function diff_prettyHtml($diffs)
    {
        $this->diff->setChanges($diffs);
        return $this->diff->prettyHtml();
    }

    /**
     * Locate the best instance of 'pattern' in 'text' near 'loc'.
     *
     * @param string $text    The text to search.
     * @param string $pattern The pattern to search for.
     * @param int    $loc     The location to search around.
     *
     * @throws \InvalidArgumentException If null inout.
     * @return int Best match index or -1.
     */
    public function match_main($text, $pattern, $loc = 0)
    {
        return $this->match->main($text, $pattern, $loc);
    }

    /**
     * Compute a list of patches to turn text1 into text2.
     * Use diffs if provided, otherwise compute it ourselves.
     * There are four ways to call this function, depending on what data is
     * available to the caller:
     * Method 1:
     *     a = text1, b = text2
     * Method 2:
     *     a = diffs
     * Method 3 (optimal):
     *     a = text1, b = diffs
     * Method 4 (deprecated, use method 3):
     *     a = text1, b = text2, c = diffs
     *
     * @param string|array      $a text1 (methods 1,3,4) or Array of diff arrays for text1 to text2 (method 2).
     * @param string|array|null $b text2 (methods 1,4) or Array of diff arrays for text1 to text2 (method 3)
     *                             or null (method 2).
     * @param array|null        $c Array of diff arrays for text1 to text2 (method 4) or null (methods 1,2,3).
     *
     * @throws \InvalidArgumentException If unknown call format.
     * @return PatchObject[] Array of PatchObjects.
     */
    public function patch_make($a, $b = null, $c = null)
    {
        return $this->patch->make($a, $b, $c);
    }

    /**
     * Take a list of patches and return a textual representation.
     *
     * @param PatchObject[] $patches Array of PatchObjects.
     *
     * @return string Text representation of patches.
     */
    public function patch_toText($patches)
    {
        return $this->patch->toText($patches);
    }

    /**
     * Parse a textual representation of patches and return a list of patch objects.
     *
     * @param string $text Text representation of patches.
     *
     * @throws \InvalidArgumentException If invalid input.
     * @throws \UnexpectedValueException If text has bad syntax.
     * @return PatchObject[] Array of PatchObjects.
     */
    public function patch_fromText($text)
    {
       return $this->patch->fromText($text);
    }

    /**
     * Merge a set of patches onto the text.  Return a patched text, as well
     * as a list of true/false values indicating which patches were applied.
     *
     * @param PatchObject[] $patches Array of PatchObjects.
     * @param string        $text    Old text.
     *
     * @return array Two element Array, containing the new text and an array of boolean values.
     */
    public function patch_apply($patches, $text)
    {
        return $this->patch->apply($patches, $text);
    }
}


<?php
/*
 * DiffMatchPatch is a port of the google-diff-match-patch (http://code.google.com/p/google-diff-match-patch/)
 * lib to PHP.
 *
 * (c) 2006 Google Inc.
 * (c) 2013 Daniil Skrobov <yetanotherape@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace DiffMatchPatch;

/**
 * Common utilities.
 *
 * @package DiffMatchPatch
 * @author Daniil Skrobov <yetanotherape@gmail.com>
 */
class Utils
{
    /**
     * Multibyte replacement for standard chr()
     *
     * @param int $code Character code.
     *
     * @return string Char with given code
     */
    public static function unicodeChr($code) {
        // TODO this works by order of magnitude slower then chr()
        $code = sprintf("%04x", $code);
        $char = json_decode('"\u'.$code.'"');
        $char = iconv('UTF-8', mb_internal_encoding(), $char);

        return $char;
    }

    /**
     * Multibyte replacement for standard ord()
     *
     * @param string $char Char.
     *
     * @return int Code of given char.
     */
    public static function unicodeOrd($char) {
        if (mb_internal_encoding() != 'UCS-2LE') {
            $char = iconv(mb_internal_encoding(), 'UCS-2LE', $char);
        }

        $code = 0;
        for ($i = 0; $i < strlen($char); $i++) {
            $code += ord($char[$i]) * pow(256, $i);
        }

        return $code;
    }

    /**
     * Special string encoding function like urlencode(),
     * corresponding to Python's urllib.parse.quote(string, "!~*'();/?:@&=+$,# ")
     *
     * @param string $string String for encoding.
     *
     * @return string Encoded string.
     */
    public static function escapeString($string)
    {
        $string = rawurlencode($string);
        $string = strtr($string, array(
            '%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')', '%3B' => ';', '%2F' => '/', '%3F' => '?',
            '%3A' => ':', '%40' => '@', '%26' => '&', '%3D' => '=', '%2B' => '+', '%24' => '$', '%2C' => ',', '%23' => '#', '%20' => ' '
        ));
        return $string;
    }

    /**
     * Special string decoding function like urldecode(),
     * corresponding to Python's urllib.parse.unquote(string)
     *
     * @param string $string String for decoding.
     *
     * @return string Decoded string.
     */
    public static function unescapeString($string)
    {
        $string = strtr($string, array(
            '%21' => '%2521', '%2A' => '%252A', '%27' => "%2527", '%28' => '%2528', '%29' => '%2529', '%3B' => '%253B', '%2F' => '%252F', '%3F' => '%253F',
            '%3A' => '%253A', '%40' => '%2540', '%26' => '%2526', '%3D' => '%253D', '%2B' => '%252B', '%24' => '%2524', '%2C' => '%252C', '%23' => '%2523', '%20' => '%2520'
        ));
        $string = rawurldecode($string);
        return $string;
    }
}


<?php

namespace NotificationChannels\SmscRu;

use Illuminate\Notifications\Notification;
use NotificationChannels\SmscRu\Exceptions\CouldNotSendNotification;

class SmscRuChannel
{
    /** @var SmscRuApi */
    protected $smsc;

    public function __construct(SmscRuApi $smsc)
    {
        $this->smsc = $smsc;
    }

    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  Notification  $notification
     *
     * @return void
     */
    public function send($notifiable, Notification $notification)
    {
        if (! ($to = $this->getRecipients($notifiable, $notification))) {
            return;
        }

        $message = $notification->{'toSmscRu'}($notifiable);

        if (\is_string($message)) {
            $message = new SmscRuMessage($message);
        }

        $this->sendMessage($to, $message);
    }

    /**
     * Gets a list of phones from the given notifiable.
     *
     * @param  mixed  $notifiable
     * @param  Notification  $notification
     *
     * @return string[]
     */
    protected function getRecipients($notifiable, Notification $notification)
    {
        $to = $notifiable->routeNotificationFor('smscru', $notification);

        if ($to === null || $to === false || $to === '') {
            return [];
        }

        return \is_array($to) ? $to : [$to];
    }

    protected function sendMessage($recipients, SmscRuMessage $message)
    {
        if (\mb_strlen($message->content) > 800) {
            throw CouldNotSendNotification::contentLengthLimitExceeded();
        }

        $params = [
            'phones'  => \implode(',', $recipients),
            'mes'     => $message->content,
            'sender'  => $message->from,
        ];

        if ($message->sendAt instanceof \DateTimeInterface) {
            $params['time'] = '0'.$message->sendAt->getTimestamp();
        }

        $this->smsc->send($params);
    }
}


<?php

namespace Bkwld\LaravelPug;

// Dependencies
use Illuminate\View\Engines\CompilerEngine;
use Pug\Assets;
use Pug\Pug;

class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
    /**
     * @var Assets
     */
    protected $assets;

    protected function setDefaultOption(Pug $pug, $name, $value)
    {
        if (method_exists($pug, 'hasOption') && !$pug->hasOption($name)) {
            $pug->setCustomOption($name, call_user_func($value));

            return;
        }

        // @codeCoverageIgnoreStart
        try {
            $pug->getOption($name);
        } catch (\InvalidArgumentException $exception) {
            $pug->setCustomOption($name, call_user_func($value));
        }
        // @codeCoverageIgnoreEnd
    }

    protected function getDefaultCache()
    {
        return storage_path($this->version() >= 5 ? '/framework/views' : '/views');
    }

    protected function getAssetsDirectories()
    {
        return array_map(function ($params) {
            list($function, $arg) = $params;

            return function_exists($function) ? call_user_func($function, $arg) : null;
        }, array(
            array('resource_path', 'assets'),
            array('app_path', 'views/assets'),
            array('app_path', 'assets'),
            array('app_path', 'views'),
            array('app_path', ''),
            array('base_path', ''),
        ));
    }

    protected function getPugEngine()
    {
        $config = $this->getConfig();

        if (!isset($config['basedir']) && function_exists('resource_path')) {
            $config['basedir'] = resource_path('views');
        }

        if (!isset($config['extensions']) && $this->app['view']) {
            $extensions = array_keys(array_filter($this->app['view']->getExtensions(), function ($engine) {
                $engines = explode('.', $engine);

                return in_array('pug', $engines) || in_array('jade', $engines);
            }));

            $config['extensions'] = array_map(function ($extension) {
                return ".$extension";
            }, $extensions);
        }

        $pug = new Pug($config);
        $this->assets = new Assets($pug);
        $getEnv = array('App', 'environment');
        $this->assets->setEnvironment(is_callable($getEnv) ? call_user_func($getEnv) : 'production');

        // Determine the cache dir if not configured
        $this->setDefaultOption($pug, 'defaultCache', array($this, 'getDefaultCache'));

        // Determine assets input directory
        $this->setDefaultOption($pug, 'assetDirectory', array($this, 'getAssetsDirectories'));

        // Determine assets output directory
        $this->setDefaultOption($pug, 'outputDirectory', array($this, 'getOutputDirectory'));

        return $pug;
    }

    protected function getPugAssets()
    {
        return $this->app['laravel-pug.pug'] ? $this->assets : null;
    }

    protected function getOutputDirectory()
    {
        return function_exists('public_path') ? public_path() : null;
    }

    protected function getCompilerCreator($compilerClass)
    {
        return function ($app) use ($compilerClass) {
            return new $compilerClass(
                array($app, 'laravel-pug.pug'),
                $app['files'],
                $this->getConfig(),
                $this->getDefaultCache()
            );
        };
    }

    /**
     * Get the major Laravel version number.
     *
     * @return int
     */
    public function version()
    {
        $app = $this->app;
        $tab = explode('Laravel Components ', $app->version());

        return intval(empty($tab[1]) ? $app::VERSION : $tab[1]);
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        // Version specific registering
        if ($this->version() >= 5) {
            $this->registerLaravel5();
        }

        // Bind the pug assets module
        $this->app->singleton('laravel-pug.pug-assets', function () {
            return $this->getPugAssets();
        });

        // Bind the package-configured Pug instance
        $this->app->singleton('laravel-pug.pug', function () {
            return $this->getPugEngine();
        });

        // Bind the Pug compiler
        $this->app->singleton(
            'Bkwld\LaravelPug\PugCompiler',
            $this->getCompilerCreator('\Bkwld\LaravelPug\PugCompiler')
        );

        // Bind the Pug Blade compiler
        $this->app->singleton(
            'Bkwld\LaravelPug\PugBladeCompiler',
            $this->getCompilerCreator('\Bkwld\LaravelPug\PugBladeCompiler')
        );
    }

    /**
     * Register specific logic for Laravel 5. Merges package config with user config.
     *
     * @return void
     */
    public function registerLaravel5()
    {
        $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'laravel-pug');
    }

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        // Version specific booting
        switch ($this->version()) {
            case 4: $this->bootLaravel4(); break;
            case 5: $this->bootLaravel5(); break;
            default: throw new Exception('Unsupported Laravel version.');
        }

        // Register compilers
        $this->registerPugCompiler();
        $this->registerPugBladeCompiler();
    }

    /**
     * Boot specific logic for Laravel 4. Tells Laravel about the package for auto
     * namespacing of config files.
     *
     * @return void
     */
    public function bootLaravel4()
    {
        $this->package('bkwld/laravel-pug');
    }

    /**
     * Boot specific logic for Laravel 5. Registers the config file for publishing
     * to app directory.
     *
     * @return void
     */
    public function bootLaravel5()
    {
        if (function_exists('config_path')) {
            $this->publishes(array(
                __DIR__ . '/../config/config.php' => config_path('laravel-pug.php'),
            ), 'laravel-pug');
        }
    }

    /**
     * Returns the view engine resolver according to current framework (laravel/lumen).
     *
     * @return \Illuminate\View\Engines\EngineResolver
     */
    public function getEngineResolver()
    {
        return isset($this->app['view.engine.resolver'])
            ? $this->app['view.engine.resolver']
            : $this->app['view']->getEngineResolver();
    }

    /**
     * Register the regular Pug compiler.
     *
     * @return void
     */
    public function registerPugCompiler($subExtension = '')
    {
        // Add resolver
        $this->getEngineResolver()->register('pug' . $subExtension, function () use ($subExtension) {
            return new CompilerEngine($this->app['Bkwld\LaravelPug\Pug' . ucfirst(ltrim($subExtension, '.')) . 'Compiler']);
        });

        // Add extensions
        $this->app['view']->addExtension('pug' . $subExtension, 'pug' . $subExtension);
        $this->app['view']->addExtension('pug' . $subExtension . '.php', 'pug' . $subExtension);
        $this->app['view']->addExtension('jade' . $subExtension, 'pug' . $subExtension);
        $this->app['view']->addExtension('jade' . $subExtension . '.php', 'pug' . $subExtension);
    }

    /**
     * Register the blade compiler compiler.
     *
     * @return void
     */
    public function registerPugBladeCompiler()
    {
        $this->registerPugCompiler('.blade');
    }

    /**
     * Get the configuration, which is keyed differently in L5 vs l4.
     *
     * @return array
     */
    public function getConfig()
    {
        $key = $this->version() >= 5 ? 'laravel-pug' : 'laravel-pug::config';

        return array_merge(array(
            'allow_composite_extensions' => true,
        ), $this->app->make('config')->get($key));
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return array(
            'Bkwld\LaravelPug\PugCompiler',
            'Bkwld\LaravelPug\PugBladeCompiler',
            'laravel-pug.pug',
        );
    }
}


<?php

namespace Bkwld\LaravelPug;

use Illuminate\Filesystem\Filesystem;
use InvalidArgumentException;
use Pug\Pug;

trait PugHandlerTrait
{
    /**
     * @var array
     */
    protected $pugTarget;

    /**
     * @var Pug
     */
    protected $pug;

    /**
     * Common pug compiler constructor.
     *
     * @param array      $pugTarget
     * @param Filesystem $files
     * @param array      $config
     */
    public function construct(array $pugTarget, Filesystem $files, array $config, $defaultCachePath = null)
    {
        $this->pugTarget = $pugTarget;
        $cachePath = null;

        foreach (array('cache_dir', 'cache', 'defaultCache') as $name) {
            if (isset($config[$name])) {
                $cachePath = $config[$name];

                break;
            }
        }

        if (!$cachePath) {
            $cachePath = $defaultCachePath ?: $this->getCachePath();
        }

        parent::__construct($files, $cachePath);
    }

    /**
     * Lazy load Pug and return the instance.
     *
     * @return Pug
     */
    public function getPug()
    {
        if (!$this->pug) {
            $this->pug = $this->pugTarget[0][$this->pugTarget[1]];
        }

        return $this->pug;
    }

    /**
     * Returns cache path.
     *
     * @return string $cachePath
     */
    public function getCachePath()
    {
        if ($this->cachePath) {
            return $this->cachePath;
        }

        $cachePath = $this->getOption('cache');

        return is_string($cachePath) ? $cachePath : $this->getOption('defaultCache');
    }

    /**
     * Get an option from pug engine or default value.
     *
     * @param string $name
     * @param null   $default
     *
     * @return mixed|null
     */
    public function getOption($name, $default = null)
    {
        $pug = $this->getPug();

        try {
            if (method_exists($pug, 'hasOption') && !$pug->hasOption($name)) {
                throw new \InvalidArgumentException('invalid option');
            }

            return $pug->getOption($name);
        } catch (\InvalidArgumentException $exception) {
            return $default;
        }
    }

    /**
     * @param string $cachePath
     */
    public function setCachePath($cachePath)
    {
        $this->cachePath = $cachePath;
        $this->getPug()->setOption('cache', $cachePath);
    }

    /**
     * Returns true if the path has an expired imports linked.
     *
     * @param $path
     *
     * @return bool
     */
    private function hasExpiredImport($path)
    {
        $compiled = $this->getCompiledPath($path);
        $importsMap = $compiled . '.imports.serialize.txt';
        $files = $this->files;

        if (!$files->exists($importsMap)) {
            return true;
        }

        $importPaths = unserialize($files->get($importsMap));
        $time = $files->lastModified($compiled);

        foreach ($importPaths as $importPath) {
            if (!$files->exists($importPath) || $files->lastModified($importPath) >= $time) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine if the view at the given path is expired.
     *
     * @param string $path
     *
     * @return bool
     */
    public function isExpired($path)
    {
        if (!$this->cachePath || parent::isExpired($path)) {
            return true;
        }

        return is_subclass_of('\Pug\Pug', '\Phug\Renderer') && $this->hasExpiredImport($path);
    }

    /**
     * Return path and set it or get it from the instance.
     *
     * @param string $path
     *
     * @throws \InvalidArgumentException
     *
     * @return string
     */
    public function extractPath($path)
    {
        if ($path && method_exists($this, 'setPath')) {
            $this->setPath($path);
        }

        if (!$path && method_exists($this, 'getPath')) {
            $path = $this->getPath();
        }

        if (!$path) {
            throw new InvalidArgumentException('Missing path argument.');
        }

        return $path;
    }

    /**
     * Returns the object the more appropriate to compile (\Pug\Pug with version < 3), \Phug\Compilser for >= 3.
     *
     * @return \Phug\CompilerInterface|Pug
     */
    public function getCompiler()
    {
        $pug = $this->getPug();

        if ($pug instanceof \Phug\Renderer) {
            $pug = clone $pug->getCompiler();
        }

        return $pug;
    }

    /**
     * Compile the view at the given path.
     *
     * @param string        $path
     * @param callable|null $callback
     *
     * @throws \InvalidArgumentException
     *
     * @return void
     */
    public function compileWith($path, callable $callback = null)
    {
        $path = $this->extractPath($path);

        if ($this->cachePath) {
            $pug = $this->getCompiler();
            $compiled = $this->getCompiledPath($path);
            $contents = $pug->compile($this->files->get($path), $path);

            if ($callback) {
                $contents = call_user_func($callback, $contents);
            }

            if ($pug instanceof \Phug\Compiler) {
                $this->files->put(
                    $compiled . '.imports.serialize.txt',
                    serialize($pug->getCurrentImportPaths())
                );
            }

            $this->files->put($compiled, $contents);
        }
    }
}


<?php

namespace Bkwld\LaravelPug;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Compilers\CompilerInterface;

class PugBladeCompiler extends BladeCompiler implements CompilerInterface
{
    use PugHandlerTrait;

    /**
     * Create a new compiler instance.
     *
     * @param array      $pugTarget
     * @param Filesystem $files
     * @param array      $config
     * @param string     $defaultCachePath
     */
    public function __construct(array $pugTarget, Filesystem $files, array $config, $defaultCachePath = null)
    {
        $this->construct($pugTarget, $files, $config, $defaultCachePath);
    }

    protected function enableBladeDirectives()
    {
        /** @var \Illuminate\View\Compilers\BladeCompiler $blade */
        $blade = Blade::getFacadeRoot();

        if ($blade && method_exists($blade, 'getCustomDirectives')) {
            foreach ($blade->getCustomDirectives() as $name => $directive) {
                $this->directive($name, $directive);
            }
        }
    }

    /**
     * Compile the view at the given path.
     *
     * @param string $path
     *
     * @throws \InvalidArgumentException
     *
     * @return void
     */
    public function compile($path = null)
    {
        $app = Blade::getFacadeApplication();

        if (isset($app['view'])) {
            $this->enableBladeDirectives();
        }

        $this->footer = array();
        $this->compileWith($path, array($this, 'compileString'));
    }
}


<?php

/*
 * This file is part of Laravel AmoCrm.
 *
 * (c) dotzero <mail@dotzero.ru>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Dotzero\LaravelAmoCrm;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Container\Container;

/**
 * This is the AmoCrm service provider class.
 *
 * @package Dotzero\LaravelAmoCrm
 * @author dotzero <mail@dotzero.ru>
 * @link http://www.dotzero.ru/
 * @link https://github.com/dotzero/laravel-amocrm
 */
class AmoCrmServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        $config = realpath(__DIR__ . '/../config/amocrm.php');

        $this->publishes([
            $config => config_path('amocrm.php'),
        ], 'config');

        $this->mergeConfigFrom($config, 'amocrm');
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('amocrm', function (Container $app) {
            $config = $app['config'];
            return new AmoCrmManager($config);
        });

        $this->app->singleton('amocrm.fields', function (Container $app) {
            $manager = $app['amocrm'];

            return $manager->getFields();
        });

        $this->app->singleton('amocrm.b2bfamily', function (Container $app) {
            $manager = $app['amocrm'];

            return $manager->getB2BFamily();
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'amocrm',
            'amocrm.fields',
            'amocrm.b2bfamily',
        ];
    }
}


<?php

/*
 * This file is part of Laravel AmoCrm.
 *
 * (c) dotzero <mail@dotzero.ru>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Dotzero\LaravelAmoCrm;

use AmoCRM\Client;
use AmoCRM\Helpers\Fields;
use AmoCRM\Helpers\B2BFamily;
use Illuminate\Contracts\Config\Repository;

/**
 * This is the AmoCrm manager class.
 *
 * @package Dotzero\LaravelAmoCrm
 * @author dotzero <mail@dotzero.ru>
 * @link http://www.dotzero.ru/
 * @link https://github.com/dotzero/laravel-amocrm
 * @property \AmoCRM\Models\Account $account
 * @property \AmoCRM\Models\Call $call
 * @property \AmoCRM\Models\Catalog $catalog
 * @property \AmoCRM\Models\CatalogElement $catalog_element
 * @property \AmoCRM\Models\Company $company
 * @property \AmoCRM\Models\Contact $contact
 * @property \AmoCRM\Models\Customer $customer
 * @property \AmoCRM\Models\CustomersPeriods $customers_periods
 * @property \AmoCRM\Models\CustomField $custom_field
 * @property \AmoCRM\Models\Lead $lead
 * @property \AmoCRM\Models\Links $links
 * @property \AmoCRM\Models\Note $note
 * @property \AmoCRM\Models\Pipelines $pipelines
 * @property \AmoCRM\Models\Task $task
 * @property \AmoCRM\Models\Transaction $transaction
 * @property \AmoCRM\Models\Unsorted $unsorted
 * @property \AmoCRM\Models\Webhooks $webhooks
 * @property \AmoCRM\Models\Widgets $widgets
 */
class AmoCrmManager
{
    /**
     * The config instance.
     *
     * @var \Illuminate\Contracts\Config\Repository
     */
    protected $config;

    /**
     * The AmoCRM client instance.
     *
     * @var \AmoCRM\Client
     */
    protected $client;

    /**
     * Create a new manager instance.
     *
     * @param \Illuminate\Contracts\Config\Repository $config
     */
    public function __construct(Repository $config)
    {
        $this->config = $config;
    }

    /**
     * Get the config instance.
     *
     * @return \Illuminate\Contracts\Config\Repository
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Get the AmoCRM client instance.
     *
     * @return \AmoCRM\Client
     */
    public function getClient()
    {
        if (!$this->client instanceof Client) {
            $this->client = new Client(
                $this->config->get('amocrm.domain'),
                $this->config->get('amocrm.login'),
                $this->config->get('amocrm.hash')
            );
        }

        return $this->client;
    }

    /**
     * Get the AmoCRM Fields helper instance.
     *
     * @return \AmoCRM\Helpers\Fields
     */
    public function getFields()
    {
        return new Fields();
    }

    /**
     * Get the AmoCRM B2BFamily helper instance.
     *
     * @return \AmoCRM\Helpers\B2BFamily
     */
    public function getB2BFamily()
    {
        $client = $this->getClient();

        return new B2BFamily($client,
            $this->config->get('amocrm.b2bfamily.appkey'),
            $this->config->get('amocrm.b2bfamily.secret'),
            $this->config->get('amocrm.b2bfamily.email'),
            $this->config->get('amocrm.b2bfamily.password')
        );
    }

    /**
     * Dynamically pass methods to AmoCRM client instance.
     *
     * @param string $property
     * @return \AmoCRM\Models\ModelInterface
     */
    public function __get($property)
    {
        return call_user_func_array([$this->getClient(), '__get'], [$property]);
    }
}