<?php

namespace ArPHP\I18N;

/**
 * ----------------------------------------------------------------------
 *
 * Copyright (c) 2006-2025 Khaled Al-Sham'aa.
 *
 * http://www.ar-php.org
 *
 * PHP Version >= 5.6
 *
 * ----------------------------------------------------------------------
 *
 * LICENSE
 *
 * This program is open source product; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License (LGPL)
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/lgpl.txt>.
 *
 * ----------------------------------------------------------------------
 * Class Name: PHP and Arabic Language
 *
 * Filename:   ar-php.php
 *
 * Original    Author(s): Khaled Al-Sham'aa <khaled@ar-php.org>
 *
 * Purpose:    Set of PHP classes developed to enhance Arabic web
 *             applications by providing set of tools includes stem-based searching,
 *             translitiration, soundex, Hijri calendar, charset detection and
 *             converter, spell numbers, keyboard language, Muslim prayer time,
 *             auto-summarization, and more...
 *
 * ----------------------------------------------------------------------
 *
 * @desc   Set of PHP classes developed to enhance Arabic web
 *         applications by providing set of tools includes translitiration,
 *         soundex, Hijri calendar, spell numbers, keyboard language, and more...
 *
 * @author    Khaled Al-Shamaa <khaled@ar-php.org>
 * @copyright 2006-2025 Khaled Al-Shamaa
 *
 * @license   LGPL <http://www.gnu.org/licenses/lgpl.txt>
 * @version   7.0.0 released in Mar 7, 2025
 * @link      http://www.ar-php.org
 */

class Arabic
{
    /** @var string */
    public $version = '7.0.0';

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

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

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

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

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

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

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

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

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

    /** @var array<string> */
    private $umAlqoura;

    /** @var array<string> */
    private $arFinePatterns = ["/'+/u", "/([\- ])'/u", '/(.)#/u'];

    /** @var array<string> */
    private $arFineReplacements = ["'", '\\1', "\\1'\\1"];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /** @var int */
    private $arDateMode = 1;

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

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

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

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

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

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

    /** @var array<int> */
    private $arNumberSpell = [];

    /** @var int */
    private $arNumberFeminine = 1;

    /** @var int */
    private $arNumberFormat = 1;

    /** @var int */
    private $arNumberOrder = 1;

    /** @var array<array<string>> */
    private $arLogodd;

    /** @var array<array<string>> */
    private $enLogodd;

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

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

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

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

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

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

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

    /** @var int */
    private $soundexLen = 4;

    /** @var string */
    private $soundexLang = 'en';

    /** @var string */
    private $soundexCode = 'soundex';

    /** @var array<array<string>> */
    private $arGlyphs = null;

    /** @var null|string */
    private $arGlyphsVowel = null;

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

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

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

    /** @var int */
    private $arQueryMode = 0;

    /** @var int */
    private $salatYear = 1975;

    /** @var int */
    private $salatMonth = 8;

    /** @var int */
    private $salatDay = 2;

    /** @var int */
    private $salatZone = 2;

    /** @var float */
    private $salatLong = 37.15861;

    /** @var float */
    private $salatLat = 36.20278;

    /** @var int */
    private $salatElevation = 0;

    /** @var float */
    private $salatAB2 = -0.833333;

    /** @var float */
    private $salatAG2 = -18;

    /** @var float */
    private $salatAJ2 = -18;

    /** @var string */
    private $salatSchool = 'Shafi';

    /** @var string */
    private $salatView   = 'Sunni';

    /** @var array<string> */
    private $arNormalizeAlef = ['أ','إ','آ'];

    /** @var array<string> */
    private $arNormalizeDiacritics = ['َ','ً','ُ','ٌ','ِ','ٍ','ْ','ّ'];

    /** @var array<string> */
    private $arSeparators = ['.',"\n",'،','؛','(','[','{',')',']','}',',',';'];

    /** @var array<string> */
    private $arCommonChars = ['ة','ه','ي','ن','و','ت','ل','ا','س','م',
                              'e', 't', 'a', 'o', 'i', 'n', 's'];

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

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

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

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

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

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

    /** @var string */
    private $rootDirectory;

    /** @var boolean */
    private $stripTatweel = true;

    /** @var boolean */
    private $stripTanween = true;

    /** @var boolean */
    private $stripShadda = true;

    /** @var boolean */
    private $stripLastHarakat = true;

    /** @var boolean */
    private $stripWordHarakat = true;

    /** @var boolean */
    private $normaliseLamAlef = true;

    /** @var boolean */
    private $normaliseAlef = true;

    /** @var boolean */
    private $normaliseHamza = true;

    /** @var boolean */
    private $normaliseTaa = true;

    /** @var array<string> */
    private $numeralHindu = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];

    /** @var array<string> */
    private $numeralPersian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];

    /** @var array<string> */
    private $numeralArabic = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];

    /** @var object */
    private $speller = null;

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

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

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

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

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

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

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

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

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

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

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

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

    /** @var float */
    private $keyboardWeight = 1;

    /** @var float */
    private $graphicWeight = 1;

    /** @var float */
    private $phoneticWeight = 1;

    // Lazy loading flags

    /** @var boolean */
    private $arStrToTimeLoad = false;

    /** @var boolean */
    private $arTransliterateLoad = false;

    /** @var boolean */
    private $arNumbersLoad = false;

    /** @var boolean */
    private $arKeySwapLoad = false;

    /** @var boolean */
    private $arSoundexLoad = false;

    /** @var boolean */
    private $arGlyphsLoad = false;

    /** @var boolean */
    private $arQueryLoad = false;

    /** @var boolean */
    private $arSummaryLoad = false;

    /** @var boolean */
    private $arSentimentLoad = false;

    /** @var boolean */
    private $arSpellerLoad = false;

    /** @var boolean */
    private $arDialectLoad = false;

    /** @var boolean */
    private $arSimilarityLoad = false;

    /** @var boolean */
    private $arNamesLoad = false;

    /** @var boolean */
    private $arDateLoad = false;

    /** @var boolean */
    private $arPluralLoad = false;

    public function __construct()
    {
        require_once 'SarahSpell.php';

        mb_internal_encoding('UTF-8');

        $this->rootDirectory = dirname(__FILE__);

        $this->arStandardInit();
    }

    /** @return void */
    private function arPluralInit()
    {
        if ($this->arPluralLoad === false) {
            $file = $this->rootDirectory . '/data/ar_plurals.json';
            $json = json_decode(file_get_contents($file), true);
            $this->arPluralsForms = $json['arPluralsForms'];

            $this->arPluralLoad = true;
        }
    }

    /** @return void */
    private function arDateInit()
    {
        if ($this->arDateLoad === false) {
            $file = $this->rootDirectory . '/data/um_alqoura.txt';
            $this->umAlqoura = file($file, FILE_IGNORE_NEW_LINES);

            $file = $this->rootDirectory . '/data/ar_date.json';
            $this->arDateJSON = json_decode((string)file_get_contents($file), true);

            $this->arDateLoad = true;
        }
    }

    /** @return void */
    private function arNamesInit()
    {
        if ($this->arNamesLoad === false) {
            $file = $this->rootDirectory . '/data/ar_female.txt';
            $this->arFemaleNames = file($file, FILE_IGNORE_NEW_LINES);

            $file = $this->rootDirectory . '/data/ar_male.txt';
            $this->arMaleNames = file($file, FILE_IGNORE_NEW_LINES);

            $this->arNamesLoad = true;
        }
    }

    /** @return void */
    private function arSpellerInit()
    {
        if ($this->arSpellerLoad === false) {
            $this->speller = new \ArPHP\MZK\Speller();

            $this->arSpellerLoad = true;
        }
    }

    /**
     * Spell Check
     *
     * @param string $text Text input
     * @return array<string>
     * @author Moutaz Alkhatib <muotaz@gmail.com>
     */
    public function spellGetMisspelled($text)
    {
        $this->arSpellerInit();

        $ret = $this->speller->spell_check($text, false);
        return array_keys($ret['no_sugg_cache']);
    }

    /**
     * Spell Suggest Corrections
     *
     * @param string $text Text input
     * @return array<array<string, mixed>>
     * @author Moutaz Alkhatib <muotaz@gmail.com>
     */
    public function spellSuggestCorrections($text)
    {
        $this->arSpellerInit();

        $ret = $this->speller->spell_check($text, true);
        return array_map(function ($element) {
            return ["word" => $element['word'], "suggestion" => array_slice($element['sugg'], 0, 8)];
        }, $ret['suggestion_array']);
    }

    /** @return void */
    private function arStandardInit()
    {
        $this->arStandardPatterns[] = '/\r\n/u';
        $this->arStandardPatterns[] = '/([^\@])\n([^\@])/u';
        $this->arStandardPatterns[] = '/\r/u';

        $this->arStandardReplacements[] = "\n@@@\n";
        $this->arStandardReplacements[] = "\\1\n&&&\n\\2";
        $this->arStandardReplacements[] = "\n###\n";

        /**
         * النقطة، الفاصلة، الفاصلة المنقوطة،
         * النقطتان، علامتي الاستفهام والتعجب،
         * النقاط الثلاث المتتالية
         * يترك فراغ واحد بعدها جميعا
         * دون أي فراغ قبلها
         */
        $this->arStandardPatterns[] = '/\s*([\.\،\؛\:\!\؟])\s*/u';
        $this->arStandardReplacements[] = '\\1 ';

        /**
         * النقاط المتتالية عددها 3 فقط
         * (ليست نقطتان وليست أربع أو أكثر)
         */
        $this->arStandardPatterns[] = '/(\. ){2,}/u';
        $this->arStandardReplacements[] = '...';

        /**
         * الأقواس ( ) [ ] { } يترك قبلها وبعدها فراغ
         * وحيد، فيما لا يوجد بينها وبين ما بداخلها
         * أي فراغ
         */
        $this->arStandardPatterns[] = '/\s*([\(\{\[])\s*/u';
        $this->arStandardPatterns[] = '/\s*([\)\}\]])\s*/u';

        $this->arStandardReplacements[] = ' \\1';
        $this->arStandardReplacements[] = '\\1 ';

        /**
         * علامات الاقتباس "..."
         * يترك قبلها وبعدها فراغ
         * وحيد، فيما لا يوجد بينها
         * وبين ما بداخلها أي فراغ
         */
        $this->arStandardPatterns[] = '/\s*\"\s*(.+)((?<!\s)\"|\s+\")\s*/u';
        $this->arStandardReplacements[] = ' "\\1" ';

        /**
         * علامات الإعتراض -...-
         * يترك قبلها وبعدها فراغ
         * وحيد، فيما لا يوجد بينها
         * وبين ما بداخلها أي فراغ
         */
        $this->arStandardPatterns[] = '/\s*\-\s*(.+)((?<!\s)\-|\s+\-)\s*/u';
        $this->arStandardReplacements[] = ' -\\1- ';

        /**
         * لا يترك فراغ بين حرف العطف الواو وبين
         * الكلمة التي تليه
         * إلا إن كانت تبدأ بحرف الواو
         */
        $this->arStandardPatterns[] = '/\sو\s+([^و])/u';
        $this->arStandardReplacements[] = ' و\\1';

        /**
         * الواحدات الإنجليزية توضع
         * على يمين الرقم مع ترك فراغ
         */
        $this->arStandardPatterns[] = '/\s+(\w+)\s*(\d+)\s+/';
        $this->arStandardPatterns[] = '/\s+(\d+)\s*(\w+)\s+/';

        $this->arStandardReplacements[] = ' <span dir="ltr">\\2 \\1</span> ';
        $this->arStandardReplacements[] = ' <span dir="ltr">\\1 \\2</span> ';

        /**
         * النسبة المؤية دائما إلى يسار الرقم
         * وبدون أي فراغ يفصل بينهما 40% مثلا
         */
        $this->arStandardPatterns[] = '/\s+(\d+)\s*\%\s+/u';
        $this->arStandardPatterns[] = '/\n?@@@\n?/u';
        $this->arStandardPatterns[] = '/\n?&&&\n?/u';
        $this->arStandardPatterns[] = '/\n?###\n?/u';

        $this->arStandardReplacements[] = ' %\\1 ';
        $this->arStandardReplacements[] = "\r\n";
        $this->arStandardReplacements[] = "\n";
        $this->arStandardReplacements[] = "\r";
    }

    /** @return void */
    private function arStrToTimeInit()
    {
        if ($this->arStrToTimeLoad === false) {
            $this->arDateInit();

            $this->strToTimeSearch = file($this->rootDirectory . '/data/strtotime_search.txt', FILE_IGNORE_NEW_LINES);
            $this->strToTimeReplace = file($this->rootDirectory . '/data/strtotime_replace.txt', FILE_IGNORE_NEW_LINES);

            foreach ($this->arDateJSON['ar_hj_month'] as $month) {
                $this->hj[] = (string)$month;
            }

            $this->strToTimePatterns[] = '/َ|ً|ُ|ٌ|ِ|ٍ|ْ|ّ/';
            $this->strToTimePatterns[] = '/\s*ال(\S{3,})\s+ال(\S{3,})/';
            $this->strToTimePatterns[] = '/\s*ال(\S{3,})/';

            $this->strToTimeReplacements[] = '';
            $this->strToTimeReplacements[] = ' \\2 \\1';
            $this->strToTimeReplacements[] = ' \\1';

            $this->arStrToTimeLoad = true;
        }
    }

    /** @return void */
    private function arTransliterateInit()
    {
        if ($this->arTransliterateLoad === false) {
            $file = $this->rootDirectory . '/data/ar_transliteration.json';
            $json = json_decode((string)file_get_contents($file), true);

            foreach ($json['preg_replace_en2ar'] as $item) {
                $this->en2arPregSearch[]  = $item['search'];
                $this->en2arPregReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_en2ar'] as $item) {
                $this->en2arStrSearch[]  = $item['search'];
                $this->en2arStrReplace[] = $item['replace'];
            }

            foreach ($json['preg_replace_ar2en'] as $item) {
                $this->ar2enPregSearch[]  = $item['search'];
                $this->ar2enPregReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_ar2en'] as $item) {
                $this->ar2enStrSearch[]  = $item['search'];
                $this->ar2enStrReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_diaritical'] as $item) {
                $this->diariticalSearch[]  = $item['search'];
                $this->diariticalReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_RJGC'] as $item) {
                $this->rjgcSearch[]  = $item['search'];
                $this->rjgcReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_SES'] as $item) {
                $this->sesSearch[]  = $item['search'];
                $this->sesReplace[] = $item['replace'];
            }

            foreach ($json['str_replace_ISO233'] as $item) {
                $this->iso233Search[]  = $item['search'];
                $this->iso233Replace[] = $item['replace'];
            }

            $this->arTransliterateLoad = true;
        }
    }

    /** @return void */
    private function arNumbersInit()
    {
        if ($this->arNumbersLoad === false) {
            $file = $this->rootDirectory . '/data/ar_numbers.json';
            $json = json_decode((string)file_get_contents($file), true);

            foreach ($json['individual']['male'] as $num) {
                if (isset($num['grammar'])) {
                    $grammar = $num['grammar'];
                    $this->arNumberIndividual["{$num['value']}"][1]["$grammar"] = (string)$num['text'];
                } else {
                    $this->arNumberIndividual["{$num['value']}"][1] = (string)$num['text'];
                }
            }

            foreach ($json['individual']['female'] as $num) {
                if (isset($num['grammar'])) {
                    $grammar = $num['grammar'];
                    $this->arNumberIndividual["{$num['value']}"][2]["$grammar"] = (string)$num['text'];
                } else {
                    $this->arNumberIndividual["{$num['value']}"][2] = (string)$num['text'];
                }
            }

            foreach ($json['individual']['gt19'] as $num) {
                if (isset($num['grammar'])) {
                    $grammar = $num['grammar'];
                    $this->arNumberIndividual["{$num['value']}"]["$grammar"] = (string)$num['text'];
                } else {
                    $this->arNumberIndividual["{$num['value']}"] = (string)$num['text'];
                }
            }

            foreach ($json['complications'] as $num) {
                $scale  = $num['scale'];
                $format = $num['format'];
                $this->arNumberComplications["$scale"]["$format"] = (string)$num['text'];
            }

            foreach ($json['arabicIndic'] as $html) {
                $value  = $html['value'];
                $this->arNumberArabicIndic["$value"] = $html['text'];
            }

            foreach ($json['order']['male'] as $num) {
                $this->arNumberOrdering["{$num['value']}"][1] = (string)$num['text'];
            }

            foreach ($json['order']['female'] as $num) {
                $this->arNumberOrdering["{$num['value']}"][2] = (string)$num['text'];
            }

            foreach ($json['individual']['male'] as $num) {
                if ($num['value'] < 11) {
                    $str = strtr((string)$num['text'], ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا']);
                    $this->arNumberSpell[$str] = (int)$num['value'];
                }
            }

            foreach ($json['individual']['female'] as $num) {
                if ($num['value'] < 11) {
                    $str = strtr((string)$num['text'], ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا']);
                    $this->arNumberSpell[$str] = (int)$num['value'];
                }
            }

            foreach ($json['individual']['gt19'] as $num) {
                $str = strtr((string)$num['text'], ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا']);
                $this->arNumberSpell[$str] = (int)$num['value'];
            }

            foreach ($json['currency'] as $money) {
                $this->arNumberCurrency[$money['iso']]['ar']['basic']    = $money['ar_basic'];
                $this->arNumberCurrency[$money['iso']]['ar']['fraction'] = $money['ar_fraction'];
                $this->arNumberCurrency[$money['iso']]['en']['basic']    = $money['en_basic'];
                $this->arNumberCurrency[$money['iso']]['en']['fraction'] = $money['en_fraction'];

                $this->arNumberCurrency[$money['iso']]['decimals'] = $money['decimals'];
            }

            $this->arNumbersLoad = true;
        }
    }

    /** @return void */
    private function arKeySwapInit()
    {
        if ($this->arKeySwapLoad === false) {
            $file = $this->rootDirectory . '/data/ar_keyswap.json';
            $json = json_decode((string)file_get_contents($file), true);

            foreach ($json['arabic'] as $key) {
                $index = (int)$key['id'];
                $this->arKeyboard[$index] = (string)$key['text'];
            }

            foreach ($json['english'] as $key) {
                $index = (int)$key['id'];
                $this->enKeyboard[$index] = (string)$key['text'];
            }

            foreach ($json['french'] as $key) {
                $index = (int)$key['id'];
                $this->frKeyboard[$index] = (string)$key['text'];
            }

            $file = $this->rootDirectory . '/data/logodd_ar.txt';
            $this->arLogodd = unserialize(file_get_contents($file));

            $file = $this->rootDirectory . '/data/logodd_en.txt';
            $this->enLogodd = unserialize(file_get_contents($file));

            $this->arKeySwapLoad = true;
        }
    }

    /** @return void */
    private function arSoundexInit()
    {
        if ($this->arSoundexLoad === false) {
            $file = $this->rootDirectory . '/data/ar_soundex.json';
            $json = json_decode((string)file_get_contents($file), true);

            foreach ($json['arSoundexCode'] as $item) {
                $index = $item['search'];
                $this->arSoundexCode["$index"] = (string)$item['replace'];
            }

            foreach ($json['arPhonixCode'] as $item) {
                $index = $item['search'];
                $this->arPhonixCode["$index"] = (string)$item['replace'];
            }

            foreach ($json['soundexTransliteration'] as $item) {
                $index = $item['search'];
                $this->soundexTransliteration["$index"] = (string)$item['replace'];
            }

            $this->soundexMap = $this->arSoundexCode;

            $this->arSoundexLoad = true;
        }
    }

    /** @return void */
    private function arGlyphsInit()
    {
        if ($this->arGlyphsLoad === false) {
            $this->arGlyphsVowel     = 'ًٌٍَُِّْ';

            // Arabic Presentation Forms-B (https://en.wikipedia.org/wiki/Arabic_Presentation_Forms-B)
            // Contextual forms (https://en.wikipedia.org/wiki/Arabic_script_in_Unicode#Contextual_forms)
            // 0- ISOLATED FORM, 1- FINAL FORM, 2- INITIAL FORM, 3- MEDIAL FORM
            $file = $this->rootDirectory . '/data/ar_glyphs.json';
            $this->arGlyphs = json_decode((string)file_get_contents($file), true);

            $this->arGlyphsLoad = true;
        }
    }

    /** @return void */
    private function arQueryInit()
    {
        if ($this->arQueryLoad === false) {
            $file = $this->rootDirectory . '/data/ar_query.json';
            $json = json_decode((string)file_get_contents($file), true);

            foreach ($json['preg_replace'] as $pair) {
                $this->arQueryLexPatterns[] = (string)$pair['search'];
                $this->arQueryLexReplacements[] = (string)$pair['replace'];
            }

            $this->arQueryLoad = true;
        }
    }

    /** @return void */
    private function arSummaryInit()
    {
        if ($this->arSummaryLoad === false) {
            // This common words used in cleanCommon method
            $words    = file($this->rootDirectory . '/data/ar_stopwords.txt', FILE_IGNORE_NEW_LINES);
            $en_words = file($this->rootDirectory . '/data/en_stopwords.txt', FILE_IGNORE_NEW_LINES);

            $words = array_merge($words, $en_words);

            $this->arSummaryCommonWords = $words;

            // This important words used in rankSentences method
            $words = file($this->rootDirectory . '/data/important_words.txt', FILE_IGNORE_NEW_LINES);

            $this->arSummaryImportantWords = $words;

            $this->arSummaryLoad = true;
        }
    }


    /** @return void */
    private function arSentimentInit()
    {
        if ($this->arSentimentLoad === false) {
            $this->allStems   = file($this->rootDirectory . '/data/stems.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddStem = file($this->rootDirectory . '/data/logodd_stem.txt', FILE_IGNORE_NEW_LINES);
            $this->logOdd     = file($this->rootDirectory . '/data/logodd.txt', FILE_IGNORE_NEW_LINES);

            $this->arSentimentLoad = true;
        }
    }


    /** @return void */
    private function arDialectInit()
    {
        if ($this->arDialectLoad === false) {
            $this->dialectsStems    = file($this->rootDirectory . '/data/dialects_stems.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddDialects   = file($this->rootDirectory . '/data/logodd_dialects.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddEgyptian   = file($this->rootDirectory . '/data/logodd_egyptian.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddLevantine  = file($this->rootDirectory . '/data/logodd_levantine.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddMaghrebi   = file($this->rootDirectory . '/data/logodd_maghrebi.txt', FILE_IGNORE_NEW_LINES);
            $this->logOddPeninsular = file($this->rootDirectory . '/data/logodd_peninsular.txt', FILE_IGNORE_NEW_LINES);

            $this->arDialectLoad = true;
        }
    }

    /** @return void */
    private function arSimilarityInit()
    {
        if ($this->arSimilarityLoad === false) {
            $json = json_decode((string)file_get_contents($this->rootDirectory . '/data/ar_similarity.json'), true);

            foreach ($json['keyboard'] as $item) {
                $char = $item['char'];
                $this->arKeyX["$char"] = (int)$item['x'];
                $this->arKeyY["$char"] = (int)$item['y'];
                $this->arKeyZ["$char"] = (int)$item['z'];
            }

            foreach ($json['graphic'] as $item) {
                $char = $item['char'];
                $this->arGraphGroup["$char"] = (int)$item['graph'];
            }

            foreach ($json['phonetic'] as $item) {
                $char = $item['char'];
                $this->arSoundGroup["$char"] = (int)$item['sound'];
            }

            foreach ($json['gap_penalty'] as $item) {
                $char = $item['char'];
                $this->arGapPenalty["$char"] = (int)$item['penalty'];
            }

            $this->arSimilarityLoad = true;
        }
    }

    /////////////////////////////////////// Standard //////////////////////////////////////////////

    /**
     * This function will standardize Arabic text to follow writing standards
     * (just like magazine/newspapers rules), for example spaces before and
     * after punctuations, brackets and units etc ...
     *
     * @param string $text Arabic text you would like to standardize
     *
     * @return String Standardized version of input Arabic text
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function standard($text)
    {
        $text = preg_replace($this->arStandardPatterns, $this->arStandardReplacements, $text);

        return $text;
    }


    /////////////////////////////////////// Gender ////////////////////////////////////////////////

    /**
     * Arabic Gender Guesser
     *
     * This function attempts to guess the gender of Arabic names.
     *
     * Arabic nouns are either masculine or feminine. Usually when referring to a male,
     * a masculine noun is usually used and when referring to a female, a feminine noun
     * is used. In most cases the feminine noun is formed by adding a special characters
     * to the end of the masculine noun. Its not just nouns referring to people that
     * have gender. Inanimate objects (doors, houses, cars, etc.) is either masculine or
     * feminine. Whether an inanimate noun is masculine or feminine is mostly arbitrary.
     *
     * @param string $str Arabic word you would like to check if it is feminine
     *
     * @return boolean Return true if input Arabic word is feminine
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function isFemale($str)
    {
        $this->arNamesInit();

        $female = false;
        $words  = explode(' ', $str);
        $str    = $words[0];

        $str = strtr($str, ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا']);

        $last       = mb_substr($str, -1, 1);
        $beforeLast = mb_substr($str, -2, 1);

        if ($last == 'ا' || $last == 'ة' || $last == 'ه' || $last == 'ى' || ($last == 'ء' && $beforeLast == 'ا')) {
            $female = true;
        } elseif (preg_match("/^[اإ].{2}ا.$/u", $str) || preg_match("/^[إا].ت.ا.+$/u", $str)) {
            // الأسماء على وزن إفتعال و إفعال
            $female = true;
        } elseif (array_search($str, $this->arFemaleNames) > 0) {
            $female = true;
        }

        // إستثناء الأسماء المذكرة المؤنثة تأنيث لفظي
        if (array_search($str, $this->arMaleNames) > 0) {
            $female = false;
        }

        return $female;
    }

    /////////////////////////////////////// StrToTime /////////////////////////////////////////////

    /**
     * Arabic arStrToTime Function
     *
     * Function to parse about any Arabic textual datetime description into
     * a Unix timestamp.
     *
     * The function expects to be given a string containing an Arabic date format
     * and will try to parse that format into a Unix timestamp (the number of seconds
     * since January 1 1970 00:00:00 GMT), relative to the timestamp given in now, or
     * the current time if none is supplied.
     *
     * @param string  $text The string to parse, according to the GNU Date Input Formats syntax (in Arabic).
     * @param integer $now  The timestamp used to calculate the returned value.
     *
     * @return Integer Returns a timestamp on success, FALSE otherwise
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function strtotime($text, $now)
    {
        $this->arStrToTimeInit();
        $int = 0;

        for ($i = 0; $i < 12; $i++) {
            if (strpos($text, $this->hj[$i]) > 0) {
                preg_match('/.*(\d{1,2}).*(\d{4}).*/', $text, $matches);

                $fix  = $this->mktimeCorrection($i + 1, (int)$matches[2]);
                $int  = $this->mktime(0, 0, 0, $i + 1, (int)$matches[1], (int)$matches[2], $fix);
                $temp = null;

                break;
            }
        }

        if ($int == 0) {
            $text = preg_replace($this->strToTimePatterns, $this->strToTimeReplacements, $text);
            $text = str_replace($this->strToTimeSearch, $this->strToTimeReplace, $text);

            $pattern = '[ابتثجحخدذرزسشصضطظعغفقكلمنهوي]';
            $text    = preg_replace("/$pattern/", '', $text);

            $int = strtotime($text, $now);
        }

        return $int;
    }

    /////////////////////////////////////// Mktime ////////////////////////////////////////////////

    /**
     * This will return current Unix timestamp
     * for given Hijri date (Islamic calendar)
     *
     * @param integer $hour       Time hour
     * @param integer $minute     Time minute
     * @param integer $second     Time second
     * @param integer $hj_month   Hijri month (Islamic calendar)
     * @param integer $hj_day     Hijri day   (Islamic calendar)
     * @param integer $hj_year    Hijri year  (Islamic calendar)
     * @param integer $correction To apply correction factor (+/- 1-2) to standard Hijri calendar
     *
     * @return integer Returns the current time measured in the number of
     *                 seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function mktime($hour, $minute, $second, $hj_month, $hj_day, $hj_year, $correction = 0)
    {
        list($year, $month, $day) = $this->arDateIslamicToGreg($hj_year, $hj_month, $hj_day);

        $unixTimeStamp = mktime($hour, $minute, $second, $month, $day, $year);
        $unixTimeStamp = $unixTimeStamp + 3600 * 24 * $correction;

        return $unixTimeStamp;
    }

    /**
     * This will convert given Hijri date (Islamic calendar) into Gregorian date
     *
     * @param integer $y Hijri year (Islamic calendar)
     * @param integer $m Hijri month (Islamic calendar)
     * @param integer $d Hijri day (Islamic calendar)
     *
     * @return array<int> Gregorian date [int Year, int Month, int Day]
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateIslamicToGreg($y, $m, $d)
    {
        $str = jdtogregorian($this->arDateIslamicToJd($y, $m, $d));

        list($month, $day, $year) = explode('/', $str);

        return [(int)$year, (int)$month, (int)$day];
    }

    /**
     * Calculate Hijri calendar correction using Um-Al-Qura calendar information
     *
     * @param integer $m Hijri month (Islamic calendar)
     * @param integer $y Hijri year  (Islamic calendar), valid range [1420-1500]
     *
     * @return integer Correction factor to fix Hijri calendar calculation using Um-Al-Qura calendar information
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function mktimeCorrection($m, $y)
    {
        $this->arDateInit();

        if ($y >= 1420 && $y < 1500) {
            $calc   = $this->mktime(0, 0, 0, $m, 1, $y);
            $offset = ($y - 1420) * 12 + $m;

            $d = substr($this->umAlqoura[$offset], 0, 2);
            $m = substr($this->umAlqoura[$offset], 3, 2);
            $y = substr($this->umAlqoura[$offset], 6, 4);

            $real = mktime(0, 0, 0, (int)$m, (int)$d, (int)$y);
            $diff = (int)(($real - $calc) / (3600 * 24));
        } else {
            $diff = 0;
        }

        return $diff;
    }

    /**
     * Calculate how many days in a given Hijri month
     *
     * @param integer $m         Hijri month (Islamic calendar)
     * @param integer $y         Hijri year  (Islamic calendar), valid range[1420-1500]
     * @param boolean $umAlqoura Should we implement Um-Al-Qura calendar correction
     *                           in this calculation (default value is true)
     *
     * @return integer Days in a given Hijri month
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function hijriMonthDays($m, $y, $umAlqoura = true)
    {
        if ($y >= 1420 && $y < 1500) {
            $begin = $this->mktime(0, 0, 0, $m, 1, $y);

            if ($m == 12) {
                $m2 = 1;
                $y2 = $y + 1;
            } else {
                $m2 = $m + 1;
                $y2 = $y;
            }

            $end = $this->mktime(0, 0, 0, $m2, 1, $y2);

            if ($umAlqoura === true) {
                $c1 = $this->mktimeCorrection($m, $y);
                $c2 = $this->mktimeCorrection($m2, $y2);
            } else {
                $c1 = 0;
                $c2 = 0;
            }

            $days = ($end - $begin) / (3600 * 24);
            $days = $days - $c1 + $c2;
        } else {
            $days = false;
        }

        return $days;
    }


    /////////////////////////////////////// Transliteration ///////////////////////////////////////

    /**
     * Transliterate English string into Arabic by render them in the
     * orthography of the Arabic language
     *
     * @param string $string English string you want to transliterate
     * @param string $locale Locale information (e.g. 'en_GB' or 'de_DE')
     *
     * @return String Out of vocabulary English string in Arabic characters
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function en2ar($string, $locale = 'en_US')
    {
        $this->arTransliterateInit();

        setlocale(LC_ALL, $locale);

        $string = iconv("UTF-8", "ASCII//TRANSLIT", $string);
        $string = preg_replace('/[^\w\s]/', '', $string);
        $string = strtolower($string);
        $words  = explode(' ', $string);
        $string = '';

        foreach ($words as $word) {
            // if it is el or al don't add space after
            if ($word == 'el' || $word == 'al') {
                $space = '';
            } else {
                $space = ' ';
            }

            // skip translation if it has no a-z char (i.e., just add it to the string as is)
            if (preg_match('/[a-z]/i', $word)) {
                $word = preg_replace($this->en2arPregSearch, $this->en2arPregReplace, $word);
                $word = strtr($word, array_combine($this->en2arStrSearch, $this->en2arStrReplace));
            }

            $string .= $word . $space;
        }

        return trim($string);
    }

    /**
     * Transliterate Arabic string into English by render them in the
     * orthography of the English language
     *
     * @param string $string   Arabic string you want to transliterate
     * @param string $standard Transliteration standard, default is UNGEGN and possible values are
     *                         [UNGEGN, UNGEGN+, RJGC, SES, ISO233]
     *
     * @return String Out of vocabulary Arabic string in English characters
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function ar2en($string, $standard = 'UNGEGN')
    {
        $this->arTransliterateInit();

        //$string = strtr($string, ['ة ال' => 'tul']);
        $words  = explode(' ', $string);
        $string = '';

        for ($i = 0; $i < count($words) - 1; $i++) {
            $words[$i] = strtr($words[$i], 'ة', 'ت');
        }

        foreach ($words as $word) {
            $temp = $word;

            if ($standard == 'UNGEGN+') {
                $temp = strtr($temp, array_combine($this->diariticalSearch, $this->diariticalReplace));
            } elseif ($standard == 'RJGC') {
                $temp = strtr($temp, array_combine($this->diariticalSearch, $this->diariticalReplace));
                $temp = strtr($temp, array_combine($this->rjgcSearch, $this->rjgcReplace));
            } elseif ($standard == 'SES') {
                $temp = strtr($temp, array_combine($this->diariticalSearch, $this->diariticalReplace));
                $temp = strtr($temp, array_combine($this->sesSearch, $this->sesReplace));
            } elseif ($standard == 'ISO233') {
                $temp = strtr($temp, array_combine($this->iso233Search, $this->iso233Replace));
            }

            $temp = preg_replace($this->ar2enPregSearch, $this->ar2enPregReplace, $temp);
            $temp = strtr($temp, array_combine($this->ar2enStrSearch, $this->ar2enStrReplace));
            $temp = preg_replace($this->arFinePatterns, $this->arFineReplacements, $temp);

            if (preg_match('/[a-z]/', mb_substr($temp, 0, 1))) {
                $temp = ucwords($temp);
            }

            $pos  = strpos($temp, '-');

            if ($pos > 0) {
                if (preg_match('/[a-z]/', mb_substr($temp, $pos + 1, 1))) {
                    $temp2  = substr($temp, 0, $pos);
                    $temp2 .= '-' . strtoupper($temp[$pos + 1]);
                    $temp2 .= substr($temp, $pos + 2);
                } else {
                    $temp2 = $temp;
                }
            } else {
                $temp2 = $temp;
            }

            $string .= ' ' . $temp2;
        }

        return trim($string);
    }


    /////////////////////////////////////// Date //////////////////////////////////////////////////

    /**
     * Setting value for $_arDateMode scalar
     *
     * @param integer $mode Output mode of date function where:
     *                       1) Hijri format (Islamic calendar)
     *                       2) Arabic month names used in Middle East countries
     *                       3) Arabic Transliteration of Gregorian month names
     *                       4) Both of 2 and 3 formats together
     *                       5) Libya style
     *                       6) Algeria and Tunis style
     *                       7) Morocco style
     *                       8) Hijri format (Islamic calendar) in English
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setDateMode($mode = 1)
    {
        $mode = (int) $mode;

        if ($mode > 0 && $mode < 9) {
            $this->arDateMode = $mode;
        }

        return $this;
    }

    /**
     * Getting $mode value that refer to output mode format
     *               1) Hijri format (Islamic calendar)
     *               2) Arabic month names used in Middle East countries
     *               3) Arabic Transliteration of Gregorian month names
     *               4) Both of 2 and 3 formats together
     *               5) Libyan way
     *               6) Algeria and Tunis style
     *               7) Morocco style
     *               8) Hijri format (Islamic calendar) in English
     *
     * @return Integer Value of $mode properity
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getDateMode()
    {
        return $this->arDateMode;
    }

    /**
     * Format a local time/date in Arabic string
     *
     * @param string  $format     Format string (same as PHP date function)
     * @param integer $timestamp  Unix timestamp
     * @param integer $correction To apply correction factor (+/- 1-2) to standard hijri calendar
     *
     * @return string Format Arabic date string according to given format string using the given integer timestamp
     *                or the current local time if no timestamp is given.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function date($format, $timestamp, $correction = 0)
    {
        $this->arDateInit();

        if ($this->arDateMode == 1 || $this->arDateMode == 8) {
            /** @var array<string> */
            $hj_txt_month = [];

            if ($this->arDateMode == 1) {
                foreach ($this->arDateJSON['ar_hj_month'] as $id => $month) {
                    $id++;
                    $hj_txt_month["$id"] = (string)$month;
                }
            }

            if ($this->arDateMode == 8) {
                foreach ($this->arDateJSON['en_hj_month'] as $id => $month) {
                    $id++;
                    $hj_txt_month["$id"] = (string)$month;
                }
            }

            $patterns     = [];
            $replacements = [];

            $patterns[] = 'Y';
            $patterns[] = 'y';
            $patterns[] = 'M';
            $patterns[] = 'F';
            $patterns[] = 'n';
            $patterns[] = 'm';
            $patterns[] = 'j';
            $patterns[] = 'd';

            $replacements[] = 'b1';
            $replacements[] = 'b2';
            $replacements[] = 'b3';
            $replacements[] = 'b3';
            $replacements[] = 'b4';
            $replacements[] = 'b5';
            $replacements[] = 'b6';
            $replacements[] = 'b7';

            if ($this->arDateMode == 8) {
                $patterns[] = 'S';
                $replacements[] = '';
            }

            $format = strtr($format, array_combine($patterns, $replacements));
            $str    = date($format, $timestamp);

            if ($this->arDateMode == 1) {
                $str = $this->arDateEn2ar($str);
            }

            list($y, $m, $d) = explode(' ', date('Y m d', $timestamp));
            list($hj_y, $hj_m, $hj_d) = $this->arDateGregToIslamic((int)$y, (int)$m, (int)$d);

            $hj_d += $correction;

            if ($hj_d <= 0) {
                $hj_d = $hj_d == 0 ? 30 : 29;
                // alter the $hj_m and $hj_y to refer to the previous month
                $hj_m = $hj_m == 1 ? 12 : $hj_m - 1;
                $hj_y = $hj_m == 12 ? $hj_y - 1 : $hj_y;
            } elseif ($hj_d > 30) {
                $hj_d = $hj_d == 31 ? 1 : 2;
                // alter the $hj_m and $hj_y to refer to the next month
                $hj_m = $hj_m == 12 ? 1 : $hj_m + 1;
                $hj_y = $hj_m == 1 ? $hj_y + 1 : $hj_y;
            }

            $patterns     = [];
            $replacements = [];

            $patterns[] = 'b1';
            $patterns[] = 'b2';
            $patterns[] = 'b3';
            $patterns[] = 'b4';
            $patterns[] = 'b5';
            $patterns[] = 'b6';
            $patterns[] = 'b7';

            $replacements[] = $hj_y;
            $replacements[] = substr((string)$hj_y, -2);
            $replacements[] = $hj_txt_month[$hj_m];
            $replacements[] = $hj_m;
            $replacements[] = sprintf('%02d', $hj_m);
            $replacements[] = $hj_d;
            $replacements[] = sprintf('%02d', $hj_d);

            $str = strtr($str, array_combine($patterns, $replacements));
        } elseif ($this->arDateMode == 5) {
            $year  = date('Y', $timestamp);
            $year -= 632;
            $yr    = substr("$year", -2);

            $format = strtr($format, ['Y' => (string)$year]);
            $format = strtr($format, ['y' => $yr]);

            $str = date($format, $timestamp);
            $str = $this->arDateEn2ar($str);
        } else {
            $str = date($format, $timestamp);
            $str = $this->arDateEn2ar($str);
        }
        return $str;
    }

    /**
     * Translate English date/time terms into Arabic langauge
     *
     * @param string $str Date/time string using English terms
     *
     * @return string Date/time string using Arabic terms
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateEn2ar($str)
    {
        $this->arDateInit();

        $patterns     = [];
        $replacements = [];

        $str = strtolower($str);

        foreach ($this->arDateJSON['en_day']['mode_full'] as $day) {
            $patterns[] = (string)$day;
        }

        foreach ($this->arDateJSON['ar_day'] as $day) {
            $replacements[] = (string)$day;
        }

        foreach ($this->arDateJSON['en_month']['mode_full'] as $month) {
            $patterns[] = (string)$month;
        }

        $replacements = array_merge($replacements, $this->arDateArabicMonths($this->arDateMode));

        foreach ($this->arDateJSON['en_day']['mode_short'] as $day) {
            $patterns[] = (string)$day;
        }

        foreach ($this->arDateJSON['ar_day'] as $day) {
            $replacements[] = (string)$day;
        }

        foreach ($this->arDateJSON['en_month']['mode_short'] as $m) {
            $patterns[] = (string)$m;
        }

        $replacements = array_merge($replacements, $this->arDateArabicMonths($this->arDateMode));

        foreach ($this->arDateJSON['preg_replace_en2ar'] as $p) {
            $patterns[] = (string)$p['search'];
            $replacements[] = (string)$p['replace'];
        }

        $str = strtr($str, array_combine($patterns, $replacements));

        return $str;
    }

    /**
     * Add Arabic month names to the replacement array
     *
     * @param integer $mode Naming mode of months in Arabic where:
     *                       2) Arabic month names used in Middle East countries
     *                       3) Arabic Transliteration of Gregorian month names
     *                       4) Both of 2 and 3 formats together
     *                       5) Libya style
     *                       6) Algeria and Tunis style
     *                       7) Morocco style
     *
     * @return array<string> Arabic month names in selected style
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateArabicMonths($mode)
    {
        $this->arDateInit();

        $replacements = [];

        foreach ($this->arDateJSON['ar_month']["mode_$mode"] as $month) {
            $replacements[] = (string)$month;
        }

        return $replacements;
    }

    /**
     * Convert given Gregorian date into Hijri date
     *
     * @param integer $y Year Gregorian year
     * @param integer $m Month Gregorian month
     * @param integer $d Day Gregorian day
     *
     * @return array<int> Hijri date [int Year, int Month, int Day](Islamic calendar)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateGregToIslamic($y, $m, $d)
    {
        $jd = gregoriantojd($m, $d, $y);

        list($year, $month, $day) = $this->arDateJdToIslamic($jd);

        return [$year, $month, $day];
    }

    /**
     * Convert given Julian day into Hijri date
     *
     * @param integer $jd Julian day
     *
     * @return array<int> Hijri date [int Year, int Month, int Day](Islamic calendar)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateJdToIslamic($jd)
    {
        $l = (int)$jd - 1948440 + 10632;
        $n = (int)(($l - 1) / 10631);

        $l = $l - 10631 * $n + 354;
        $j = (int)((10985 - $l) / 5316) * (int)((50 * $l) / 17719) + (int)($l / 5670) * (int)((43 * $l) / 15238);

        $l = $l - (int)((30 - $j) / 15) * (int)((17719 * $j) / 50) - (int)($j / 16) * (int)((15238 * $j) / 43) + 29;
        $m = (int)((24 * $l) / 709);
        $d = $l - (int)((709 * $m) / 24);
        $y = (int)(30 * $n + $j - 30);

        return [$y, $m, $d];
    }

    /**
     * Convert given Hijri date into Julian day
     *
     * @param integer $y Year Hijri year
     * @param integer $m Month Hijri month
     * @param integer $d Day Hijri day
     *
     * @return integer Julian day
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDateIslamicToJd($y, $m, $d)
    {
        $jd = (int)((11 * $y + 3) / 30) + (int)(354 * $y) + (int)(30 * $m) - (int)(($m - 1) / 2) + $d + 1948440 - 385;

        return $jd;
    }

    /**
     * Calculate Hijri calendar correction using Um-Al-Qura calendar information
     *
     * @param integer $time Unix timestamp
     *
     * @return integer Correction factor to fix Hijri calendar calculation using Um-Al-Qura calendar information
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function dateCorrection($time)
    {
        $this->arDateInit();

        $calc = $time - (int)$this->date('j', $time) * 3600 * 24;

        $y      = (int)$this->date('Y', $time);
        $m      = (int)$this->date('n', $time);
        $offset = ($y - 1420) * 12 + $m;

        $d = substr($this->umAlqoura[$offset], 0, 2);
        $m = substr($this->umAlqoura[$offset], 3, 2);
        $y = substr($this->umAlqoura[$offset], 6, 4);

        $real = mktime(0, 0, 0, (int)$m, (int)$d, (int)$y);
        $diff = (int)(($calc - $real) / (3600 * 24));

        return $diff;
    }


    /////////////////////////////////////// Numbers ///////////////////////////////////////////////

    /**
     * Set feminine flag of the counted object
     *
     * @param integer $value Counted object feminine (1 for masculine & 2 for feminine)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setNumberFeminine($value)
    {
        if ($value == 1 || $value == 2) {
            $this->arNumberFeminine = $value;
        }

        return $this;
    }

    /**
     * Set the grammar position flag of the counted object
     *
     * @param integer $value Grammar position of counted object (1 if Marfoua & 2 if Mansoub or Majrour)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setNumberFormat($value)
    {
        if ($value == 1 || $value == 2) {
            $this->arNumberFormat = $value;
        }

        return $this;
    }

    /**
     * Set the ordering flag, is it normal number or ordering number
     *
     * @param integer $value Is it an ordering number? default is 1 (use 1 if no and 2 if yes)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setNumberOrder($value)
    {
        if ($value == 1 || $value == 2) {
            $this->arNumberOrder = $value;
        }

        return $this;
    }

    /**
     * Get the feminine flag of counted object
     *
     * @return integer return current setting of counted object feminine flag
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getNumberFeminine()
    {
        return $this->arNumberFeminine;
    }

    /**
     * Get the grammer position flag of counted object
     *
     * @return integer return current setting of counted object grammer position flag
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getNumberFormat()
    {
        return $this->arNumberFormat;
    }

    /**
     * Get the ordering flag value
     *
     * @return integer return current setting of ordering flag value
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getNumberOrder()
    {
        return $this->arNumberOrder;
    }

    /**
     * Spell integer number in Arabic idiom
     *
     * @param integer $number The number you want to spell in Arabic idiom
     *
     * @return string The Arabic idiom that spells inserted number
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function int2str($number)
    {
        if ($number == 1 && $this->arNumberOrder == 2) {
            if ($this->arNumberFeminine == 1) {
                $string = 'الأول';
            } else {
                $string = 'الأولى';
            }
        } else {
            if ($number < 0) {
                $string = 'سالب ';
                $number = (string) (-1 * $number);
            } else {
                $string = '';
            }

            $temp = explode('.', (string)$number);

            $string .= $this->arNumbersSubStr($temp[0]);

            if (!empty($temp[1])) {
                $dec     = $this->arNumbersSubStr($temp[1]);
                $string .= ' فاصلة ' . $dec;
            }
        }

        return $string;
    }

    /**
     * Spell integer number in Arabic idiom followed by plural form of the counted item
     *
     * @param integer $count The number you want to spell in Arabic idiom
     * @param string  $word  The counted item
     *
     * @return string The Arabic idiom that spells inserted number followed by plural form of the counted item.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function int2strItem($count, $word)
    {
        $feminine = $this->isFemale($word) ? 2 : 1;

        $this->setNumberFeminine($feminine);

        $str1 = $this->int2str($count);
        $str2 = $this->arPlural($word, $count);

        $string = strtr($str2, ['%d' => $str1]);

        return $string;
    }

    /**
     * Spell number in Arabic idiom as money
     *
     * @param integer $number The number you want to spell in Arabic idiom as money
     * @param string  $iso    The three-letter Arabic country code defined in ISO 3166 standard
     * @param string  $lang   The two-letter language code in ISO 639-1 standard [ar|en]
     *
     * @return string The Arabic idiom that spells inserted number as money
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function money2str($number, $iso = 'SYP', $lang = 'ar')
    {
        $this->arNumbersInit();

        $iso  = strtoupper($iso);
        $lang = strtolower($lang);

        $number = sprintf("%01.{$this->arNumberCurrency[$iso]['decimals']}f", $number);
        $temp   = explode('.', $number);
        $string = '';

        if ((int)$temp[0] != 0) {
            if ($lang == 'ar') {
                $string .= $this->int2strItem((int)$temp[0], $this->arNumberCurrency[$iso][$lang]['basic']);
            } else {
                $string .= $temp[0] . ' ' . $this->arNumberCurrency[$iso][$lang]['basic'];
            }
        }

        if (!empty($temp[1]) && (int)$temp[1] != 0) {
            if ($string != '') {
                if ($lang == 'ar') {
                    $string .= ' و';
                } else {
                    $string .= ' and ';
                }
            }

            if ($lang == 'ar') {
                $string .= $this->int2strItem((int)$temp[1], $this->arNumberCurrency[$iso][$lang]['fraction']);
            } else {
                $string .= $temp[1] . ' ' . $this->arNumberCurrency[$iso][$lang]['fraction'];
            }
        }

        return $string;
    }

    /**
     * Convert Arabic idiom number string into Integer
     *
     * @param string $str The Arabic idiom that spells input number
     *
     * @return integer The number you spell it in the Arabic idiom
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function str2int($str)
    {
        $this->arNumbersInit();

        // Normalization phase
        $str = strtr($str, ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا']);
        $str = strtr($str, ['ه' => 'ة']);
        $str = preg_replace('/\s+/', ' ', $str);
        $ptr = ['ـ' => '', 'َ' => '', 'ً' => '', 'ُ' => '', 'ٌ' => '', 'ِ' => '', 'ٍ' => '', 'ْ' => '', 'ّ' => ''];
        $str = strtr($str, $ptr);
        $str = strtr($str, ['مائة' => 'مئة']);
        $ptr = ['/احدى\s/u','/احد\s/u'];
        $str = preg_replace($ptr, 'واحد ', $str);
        $ptr = ['/اثنا\s/u','/اثني\s/u','/اثنتا\s/u', '/اثنتي\s/u','/اثنين\s/u','/اثنتان\s/u', '/اثنتين\s/u'];
        $str = preg_replace($ptr, 'اثنان ', $str);

        $str = trim($str);

        if (strpos($str, 'ناقص') === false && strpos($str, 'سالب') === false) {
            $negative = false;
        } else {
            $negative = true;
        }

        // Complications process
        $segment = [];
        $max     = count($this->arNumberComplications);

        for ($scale = $max; $scale > 0; $scale--) {
            $key = pow(1000, $scale);

            $pattern = ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا'];
            $format1 = strtr($this->arNumberComplications[$scale][1], $pattern);
            $format2 = strtr($this->arNumberComplications[$scale][2], $pattern);
            $format3 = strtr($this->arNumberComplications[$scale][3], $pattern);
            $format4 = strtr($this->arNumberComplications[$scale][4], $pattern);

            if (strpos($str, $format1) !== false && strlen($format1) > 0) {
                list($temp, $str) = explode($format1, $str);
                $segment[$key]    = 'اثنان';
            } elseif (strpos($str, $format2) !== false && strlen($format2) > 0) {
                list($temp, $str) = explode($format2, $str);
                $segment[$key]    = 'اثنان';
            } elseif (strpos($str, $format3) !== false && strlen($format3) > 0) {
                list($segment[$key], $str) = explode($format3, $str);
            } elseif (strpos($str, $format4) !== false && strlen($format4) > 0) {
                list($segment[$key], $str) = explode($format4, $str);
                if ($segment[$key] == '') {
                    $segment[$key] = 'واحد';
                }
            }

            if (isset($segment[$key])) {
                $segment[$key] = trim($segment[$key]);
            }
        }

        $segment[1] = trim($str);
        // Individual process

        $total    = 0;
        $subTotal = 0;

        foreach ($segment as $scale => $str) {
            $str = " $str ";
            foreach ($this->arNumberSpell as $word => $value) {
                if (strpos($str, "$word ") !== false) {
                    $str = strtr($str, ["$word " => ' ']);
                    $subTotal += $value;
                }
            }

            $total   += $subTotal * $scale;
            $subTotal = 0;
        }

        if ($negative) {
            $total = -1 * $total;
        }

        return $total;
    }

    /**
     * Spell integer number in Arabic idiom
     *
     * @param string  $number The number you want to spell in Arabic idiom
     * @param boolean $zero   Present leading zero if true [default is true]
     *
     * @return string The Arabic idiom that spells inserted number
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arNumbersSubStr($number, $zero = true)
    {
        $this->arNumbersInit();

        $blocks = [];
        $items  = [];
        $zeros  = '';
        $string = '';
        $number = ($zero != false) ? trim((string)$number) : trim((string)(float)$number);

        if ($number > 0) {
            //--- by Jnom: handle left zero
            // http://www.itkane.com
            // jnom23@gmail.com
            if ($zero != false) {
                $fulnum = $number;
                while (($fulnum[0]) == '0') {
                    $zeros = 'صفر ' . $zeros;
                    $fulnum = substr($fulnum, 1, strlen($fulnum));
                };
                $zeros = trim($zeros);
            };
            //---/

            while (strlen($number) > 3) {
                $blocks[] = substr($number, -3);
                $number = substr($number, 0, strlen($number) - 3);
            }

            $blocks[] = $number;
            $blocks_num = count($blocks) - 1;

            for ($i = $blocks_num; $i >= 0; $i--) {
                $number = floor((float)$blocks[$i]);
                $text   = $this->arNumberWrittenBlock((int)$number);

                if ($text) {
                    if ($number == 1 && $i != 0) {
                        $text = $this->arNumberComplications[$i][4];
                    } elseif ($number == 2 && $i != 0) {
                        $text = $this->arNumberComplications[$i][$this->arNumberFormat];
                    } elseif ($number > 2 && $number < 11 && $i != 0) {
                        $text .= ' ' . $this->arNumberComplications[$i][3];
                    } elseif ($i != 0) {
                        $text .= ' ' . $this->arNumberComplications[$i][4];
                    }

                    if ($this->arNumberOrder == 2 && ($number > 1 && $number < 11)) {
                        $text = 'ال' . $text;
                    }

                    //--- by Jnom: handle left zero
                    if ($text != '' && $zeros != '' && $zero != false) {
                        $text  = $zeros . ' ' . $text;
                        $zeros = '';
                    };
                    //---/

                    $items[] = $text;
                }
            }

            $string = implode(' و', $items);
        } else {
            $string = 'صفر';
        }

        return $string;
    }

    /**
     * Spell sub block number of three digits max in Arabic idiom
     *
     * @param integer $number Sub block number of three digits max you want to spell in Arabic idiom
     *
     * @return string The Arabic idiom that spells inserted sub block
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arNumberWrittenBlock($number)
    {
        $this->arNumbersInit();

        $items  = [];
        $string = '';

        if ($number > 99) {
            $hundred = floor($number / 100) * 100;
            $number  = $number % 100;

            if ($this->arNumberOrder == 2) {
                $pre = 'ال';
            } else {
                $pre = '';
            }

            if ($hundred == 200) {
                $items[] = $pre . $this->arNumberIndividual[$hundred][$this->arNumberFormat];
            } else {
                $items[] = $pre . $this->arNumberIndividual[$hundred];
            }
        }

        if ($number != 0) {
            if ($this->arNumberOrder == 2) {
                if ($number <= 10) {
                    $items[] = $this->arNumberOrdering[$number][$this->arNumberFeminine];
                } elseif ($number < 20) {
                    $number -= 10;
                    $item    = 'ال' . $this->arNumberOrdering[$number][$this->arNumberFeminine];

                    if ($this->arNumberFeminine == 1) {
                        $item .= ' عشر';
                    } else {
                        $item .= ' عشرة';
                    }

                    $items[] = $item;
                } else {
                    $ones = $number % 10;
                    $tens = floor($number / 10) * 10;

                    if ($ones > 0) {
                        $items[] = 'ال' . $this->arNumberOrdering[$ones][$this->arNumberFeminine];
                    }
                    $items[] = 'ال' . $this->arNumberIndividual[$tens][$this->arNumberFormat];
                }
            } else {
                if ($number == 2 || $number == 12) {
                    $items[] = $this->arNumberIndividual[$number][$this->arNumberFeminine][$this->arNumberFormat];
                } elseif ($number < 20) {
                    $items[] = $this->arNumberIndividual[$number][$this->arNumberFeminine];
                } else {
                    $ones = $number % 10;
                    $tens = floor($number / 10) * 10;

                    if ($ones == 2) {
                        $items[] = $this->arNumberIndividual[2][$this->arNumberFeminine][$this->arNumberFormat];
                    } elseif ($ones > 0) {
                        $items[] = $this->arNumberIndividual[$ones][$this->arNumberFeminine];
                    }

                    $items[] = $this->arNumberIndividual[$tens][$this->arNumberFormat];
                }
            }
        }

        $items  = array_diff($items, ['']);
        $string = implode(' و', $items);

        return $string;
    }

    /**
     * Represent integer number in Arabic-Indic digits using HTML entities
     *
     * @param integer $number The number you want to present in Arabic-Indic digits using HTML entities
     *
     * @return string The Arabic-Indic digits represent inserted integer number using HTML entities
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function int2indic($number)
    {
        $this->arNumbersInit();

        $str = strtr("$number", $this->arNumberArabicIndic);

        return $str;
    }


    /////////////////////////////////////// KeySwap ///////////////////////////////////////////////

    /**
     * Make conversion to swap that odd Arabic text by original English sentence
     * you meant when you type on your keyboard (if keyboard language was incorrect)
     *
     * @param string $text Odd Arabic string
     *
     * @return string Normal English string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function swapAe($text)
    {
        $pieces = explode('لا', $text);

        $max = count($pieces);

        for ($i = 0; $i < $max; $i++) {
            $pieces[$i] = $this->swapCore($pieces[$i], 'ar', 'en');
        }

        if ($max > 1) {
            for ($i = 1; $i < $max; $i++) {
                $first_next = mb_substr($pieces[$i], 0, 1);
                $last_prev  = mb_substr($pieces[$i - 1], -1);

                $rank_b  = (float)$this->enLogodd[$last_prev]['b'] + (float)$this->enLogodd['b'][$first_next];
                $rank_gh = (float)$this->enLogodd[$last_prev]['g'] + (float)$this->enLogodd['h'][$first_next];

                if ($rank_b > $rank_gh) {
                    $pieces[$i] = 'b' . $pieces[$i];
                } else {
                    $pieces[$i] = 'gh' . $pieces[$i];
                }
            }
        }

        $output = implode('', $pieces);

        return $output;
    }

    /**
     * Make conversion to swap that odd English text by original Arabic sentence
     * you meant when you type on your keyboard (if keyboard language was incorrect)
     *
     * @param string $text Odd English string
     *
     * @return string Normal Arabic string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function swapEa($text)
    {
        $output = $this->swapCore($text, 'en', 'ar');

        return $output;
    }

    /**
     * Make conversion to swap that odd Arabic text by original French sentence
     * you meant when you type on your keyboard (if keyboard language was incorrect)
     *
     * @param string $text Odd Arabic string
     *
     * @return string Normal French string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function swapAf($text)
    {
        $output = $this->swapCore($text, 'ar', 'fr');

        return $output;
    }

    /**
     * Make conversion to swap that odd French text by original Arabic sentence
     * you meant when you type on your keyboard (if keyboard language was incorrect)
     *
     * @param string $text Odd French string
     *
     * @return string Normal Arabic string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function swapFa($text)
    {
        $output = $this->swapCore($text, 'fr', 'ar');

        return $output;
    }

    /**
     * Make conversion between different keyboard maps to swap odd text in
     * one language by original meaningful text in another language that
     * you meant when you type on your keyboard (if keyboard language was incorrect)
     *
     * @param string $text Odd string
     * @param string $in   Input language [ar|en|fr]
     * @param string $out  Output language [ar|en|fr]
     *
     * @return string Normal string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function swapCore($text, $in, $out)
    {
        $this->arKeySwapInit();

        $output = '';
        $text   = stripslashes($text);
        $max    = mb_strlen($text);

        $inputMap  = [];
        $outputMap = [];

        switch ($in) {
            case 'ar':
                $inputMap = $this->arKeyboard;
                break;
            case 'en':
                $inputMap = $this->enKeyboard;
                break;
            case 'fr':
                $inputMap = $this->frKeyboard;
                break;
        }

        switch ($out) {
            case 'ar':
                $outputMap = $this->arKeyboard;
                break;
            case 'en':
                $outputMap = $this->enKeyboard;
                break;
            case 'fr':
                $outputMap = $this->frKeyboard;
                break;
        }

        for ($i = 0; $i < $max; $i++) {
            $chr = mb_substr($text, $i, 1);
            $key = array_search($chr, $inputMap);

            if ($key === false) {
                $output .= $chr;
            } else {
                $output .= $outputMap[$key];
            }
        }

        return $output;
    }

    /**
     * Calculate the log odd probability that inserted string from keyboard is in English language
     *
     * @param string $str Inserted string from the keyboard
     *
     * @return float Calculated score for input string as English language
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function checkEn($str)
    {
        $this->arKeySwapInit();

        $str  = mb_strtolower($str);
        $max  = mb_strlen($str);
        $rank = 0;

        for ($i = 1; $i < $max; $i++) {
            $first  = mb_substr($str, $i - 1, 1);
            $second = mb_substr($str, $i, 1);

            if (isset($this->enLogodd["$first"]["$second"])) {
                $rank += $this->enLogodd["$first"]["$second"];
            } else {
                $rank -= 10;
            }
        }

        return $rank;
    }

    /**
     * Calculate the log odd probability that inserted string from keyboard is in Arabic language
     *
     * @param string $str Inserted string from the keyboard
     *
     * @return float Calculated score for input string as Arabic language
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function checkAr($str)
    {
        $this->arKeySwapInit();

        $max  = mb_strlen($str);
        $rank = 0;

        for ($i = 1; $i < $max; $i++) {
            $first  = mb_substr($str, $i - 1, 1);
            $second = mb_substr($str, $i, 1);

            if (isset($this->arLogodd["$first"]["$second"])) {
                $rank += $this->arLogodd["$first"]["$second"];
            } else {
                $rank -= 10;
            }
        }

        return $rank;
    }

    /**
     * This method will automatically detect the language of content supplied
     * in the input string. It will return the suggestion of correct inserted text.
     * The accuracy of the automatic language detection increases with the amount
     * of text entered.
     *
     * @param string $str Inserted string from the keyboard
     *
     * @return string Fixed string language and letter case to the better guess
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function fixKeyboardLang($str)
    {
        preg_match_all("/([\x{0600}-\x{06FF}])/u", $str, $matches);

        $arNum    = count($matches[0]);
        $nonArNum = mb_strlen(strtr($str, [' ' => ''])) - $arNum;

        $capital = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
        $small   = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

        $strCaps   = strtr($str, $capital, $small);
        $arStrCaps = $this->swapEa($strCaps);

        if ($arNum > $nonArNum) {
            $arStr = $str;
            $enStr = $this->swapAe($str);
            $isAr  = true;

            $enRank = $this->checkEn($enStr);
            $arRank = $this->checkAr($arStr);

            $arCapsRank = $arRank;
        } else {
            $arStr = $this->swapEa($str);
            $enStr = $str;

            $isAr = false;

            $enRank = $this->checkEn($enStr);
            $arRank = $this->checkAr($arStr);

            $arCapsRank = $this->checkAr($arStrCaps);
        }

        if ($enRank > $arRank && $enRank > $arCapsRank) {
            if ($isAr) {
                $fix = $enStr;
            } else {
                preg_match_all("/([A-Z])/u", $enStr, $matches);
                $capsNum = count($matches[0]);

                preg_match_all("/([a-z])/u", $enStr, $matches);
                $nonCapsNum = count($matches[0]);

                if ($capsNum > $nonCapsNum && $nonCapsNum > 0) {
                    $enCapsStr = strtr($enStr, $capital, $small);
                    $fix       = $enCapsStr;
                } else {
                    $fix = '';
                }
            }
        } else {
            if ($arCapsRank > $arRank) {
                $arStr  = $arStrCaps;
                $arRank = $arCapsRank;
            }
            if (!$isAr) {
                $fix = $arStr;
            } else {
                $fix = '';
            }
        }

        return $fix;
    }


    /////////////////////////////////////// Soundex ///////////////////////////////////////////////

    /**
     * Set the length of soundex key (default value is 4)
     *
     * @param integer $integer Soundex key length
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSoundexLen($integer)
    {
        $this->soundexLen = (int)$integer;

        return $this;
    }

    /**
     * Set the language of the soundex key (default value is "en")
     *
     * @param string $str Soundex key language [ar|en]
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSoundexLang($str)
    {
        $str = strtolower($str);

        if ($str == 'ar' || $str == 'en') {
            $this->soundexLang = $str;
        }

        return $this;
    }

    /**
     * Set the mapping code of the soundex key (default value is "soundex")
     *
     * @param string $str Soundex key mapping code [soundex|phonix]
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSoundexCode($str)
    {
        $this->arSoundexInit();

        $str = strtolower($str);

        if ($str == 'soundex' || $str == 'phonix') {
            $this->soundexCode = $str;

            if ($str == 'phonix') {
                $this->soundexMap = $this->arPhonixCode;
            } else {
                $this->soundexMap = $this->arSoundexCode;
            }
        }

        return $this;
    }

    /**
     * Get the soundex key length used now
     *
     * @return integer return current setting for soundex key length
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getSoundexLen()
    {
        return $this->soundexLen;
    }

    /**
     * Get the soundex key language used now
     *
     * @return string return current setting for soundex key language
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getSoundexLang()
    {
        return $this->soundexLang;
    }

    /**
     * Get the soundex key calculation method used now
     *
     * @return string return current setting for soundex key calculation method
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getSoundexCode()
    {
        return $this->soundexCode;
    }

    /**
     * Methode to get soundex/phonix numric code for given word
     *
     * @param string $word The word that we want to encode it
     *
     * @return string The calculated soundex/phonix numeric code
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSoundexMapCode($word)
    {
        $this->arSoundexInit();

        $encodedWord = '';
        $max         = mb_strlen($word);

        for ($i = 0; $i < $max; $i++) {
            $char = mb_substr($word, $i, 1);

            if (isset($this->soundexMap["$char"])) {
                $encodedWord .= $this->soundexMap["$char"];
            } else {
                $encodedWord .= '0';
            }
        }

        return $encodedWord;
    }

    /**
     * Remove any characters replicates
     *
     * @param string $word Arabic word you want to check if it is feminine
     *
     * @return string Same word without any duplicate chracters
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSoundexTrimRep($word)
    {
        $lastChar  = null;
        $cleanWord = null;
        $max       = mb_strlen($word);

        for ($i = 0; $i < $max; $i++) {
            $char = mb_substr($word, $i, 1);

            if ($char != $lastChar) {
                $cleanWord .= $char;
            }

            $lastChar = $char;
        }

        return $cleanWord;
    }

    /**
     * Arabic soundex algorithm takes Arabic word as an input and produces a
     * character string which identifies a set words that are (roughly)
     * phonetically alike.
     *
     * @param string $word Arabic word you want to calculate its soundex
     *
     * @return string Soundex value for a given Arabic word
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function soundex($word)
    {
        $this->arSoundexInit();

        $soundex = mb_substr($word, 0, 1);
        $rest    = mb_substr($word, 1, mb_strlen($word));

        if ($this->soundexLang == 'en') {
            $soundex = $this->soundexTransliteration[$soundex];
        }

        $encodedRest      = $this->arSoundexMapCode($rest);
        $cleanEncodedRest = $this->arSoundexTrimRep($encodedRest);

        $soundex .= $cleanEncodedRest;
        $soundex  = strtr($soundex, ['0' => '']);
        $totalLen = mb_strlen($soundex);

        if ($totalLen > $this->soundexLen) {
            $soundex = mb_substr($soundex, 0, $this->soundexLen);
        } else {
            $soundex .= str_repeat('0', $this->soundexLen - $totalLen);
        }

        return $soundex;
    }


    /////////////////////////////////////// Glyphs ////////////////////////////////////////////////

    /**
     * Add extra glyphs
     *
     * @param string  $char     Char to be added
     * @param string  $hex      String of 16 hexadecimals digits refers to the letter unicode
     *                          in the following order
     *                          ISOLATED FORM, FINAL FORM, INITIAL FORM, MEDIAL FORM
     *                          (e.g. for Arabic letter HEH 'FEE9FEEAFEEBFEEC')
     * @param boolean $prevLink If TRUE (default), when this letter be previous, then next will be linked to it
     * @param boolean $nextLink If TRUE (default), when this letter be next, then previous will be linked to it
     *
     * @return void
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function addGlyphs($char, $hex, $prevLink = true, $nextLink = true)
    {
        $this->arGlyphsInit();

        $this->arGlyphs[$char][0] = substr($hex, 0, 4);
        $this->arGlyphs[$char][1] = substr($hex, 4, 4);
        $this->arGlyphs[$char][2] = substr($hex, 8, 4);
        $this->arGlyphs[$char][3] = substr($hex, 12, 4);

        $this->arGlyphs[$char]["prevLink"] = $prevLink;
        $this->arGlyphs[$char]["nextLink"] = $nextLink;
    }

    /**
     * Convert Arabic string into glyph joining in UTF-8 hexadecimals stream
     *
     * @param string $str Arabic string in UTF-8 charset
     *
     * @return string Arabic glyph joining in UTF-8 hexadecimals stream
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arGlyphsPreConvert($str)
    {
        $this->arGlyphsInit();

        $crntChar = null;
        $prevChar = null;
        $nextChar = null;
        $output   = '';
        $number   = '';
        $chars    = [];

        $open_range  = ')]>}';
        $close_range = '([<{';

        $_temp = mb_strlen($str);

        // split the given string to an array of chars
        for ($i = 0; $i < $_temp; $i++) {
            $chars[] = mb_substr($str, $i, 1);
        }

        $max = count($chars);

        // scan the array of chars backward to flip the sequence of Arabic chars in the string
        for ($i = $max - 1; $i >= 0; $i--) {
            $crntChar = $chars[$i];

            // by default assume the letter form is isolated
            $form = 0;

            // set the prevChar by ignore tashkeel (max of two harakat), let it be space if we process the last char
            if ($i > 0) {
                $prevChar = $chars[$i - 1];
                if (mb_strpos($this->arGlyphsVowel, $prevChar) !== false && $i > 1) {
                    $prevChar = $chars[$i - 2];

                    if (mb_strpos($this->arGlyphsVowel, $prevChar) !== false && $i > 2) {
                        $prevChar = $chars[$i - 3];
                    }
                }
            } else {
                $prevChar = ' ';
            }

            // if it is a digit, then keep adding it to the number in the correct order from left to right
            // once finish, push it to the output array as a whole number then reset the number value to empty
            if (is_numeric($crntChar)) {
                $number = $crntChar . $number;
                continue;
            } elseif (strlen($number) > 0) {
                $output .= $number;
                $number  = '';
            }

            // handle the case of open and close brackets (flip them)
            if (mb_strpos($open_range . $close_range, $crntChar) !== false) {
                $output .= ($close_range . $open_range)[mb_strpos($open_range . $close_range, $crntChar)];
                continue;
            }

             // if it is an English char, then show it as it is
            if (ord($crntChar) < 128) {
                $output  .= $crntChar;
                $nextChar = $crntChar;
                continue;
            }

            // if the current char is LAM followed by ALEF, use ALEF-LAM character, then step to the next char
            if (
                $crntChar == 'ل' && isset($nextChar)
                && (mb_strpos('آأإا', $nextChar) !== false)
            ) {
                $output = substr($output, 0, strlen($output) - 8);
                if (isset($this->arGlyphs[$prevChar]['prevLink']) && $this->arGlyphs[$prevChar]['prevLink'] == true) {
                    $output .= '&#x' . $this->arGlyphs[$crntChar . $nextChar][1] . ';';
                } else {
                    $output .= '&#x' . $this->arGlyphs[$crntChar . $nextChar][0] . ';';
                }
                if ($prevChar == 'ل') {
                    if (isset($chars[$i - 2])) {
                        $tmp_form = (isset($this->arGlyphs[$chars[$i - 2]]['prevLink']) &&
                                     $this->arGlyphs[$chars[$i - 2]]['prevLink'] == true) ? 3 : 2;
                    } else {
                        $tmp_form = 2;
                    }
                    $output .= '&#x' . $this->arGlyphs[$prevChar][$tmp_form] . ';';
                    $i--;
                }
                continue;
            }

            // handle the case of HARAKAT
            if (mb_strpos($this->arGlyphsVowel, $crntChar) !== false) {
                if ($crntChar == 'ّ') {
                    if (mb_strpos($this->arGlyphsVowel, $chars[$i - 1]) !== false) {
                        // check if the SHADDA & HARAKA in the middle of connected letters (form 3)
                        if (
                            ($prevChar && $this->arGlyphs[$prevChar]['prevLink'] == true) &&
                            ($nextChar && $this->arGlyphs[$nextChar]['nextLink'] == true)
                        ) {
                            $form = 3;
                        }

                        // handle the case of HARAKAT after SHADDA
                        switch ($chars[$i - 1]) {
                            case 'ً':
                                $output .= '&#x0651;&#x064B;';
                                break;
                            case 'ٌ':
                                $output .= '&#xFC5E;';
                                break;
                            case 'ٍ':
                                $output .= '&#xFC5F;';
                                break;
                            case 'َ':
                                $output .= ($form == 3) ? '&#xFCF2;' : '&#xFC60;';
                                break;
                            case 'ُ':
                                $output .= ($form == 3) ? '&#xFCF3;' : '&#xFC61;';
                                break;
                            case 'ِ':
                                $output .= ($form == 3) ? '&#xFCF4;' : '&#xFC62;';
                                break;
                        }
                    } else {
                        $output .= '&#x0651;';
                    }
                // else show HARAKAT if it is not combined with SHADDA (which processed above)
                } elseif (!isset($chars[$i + 1]) || $chars[$i + 1] != 'ّ') {
                    switch ($crntChar) {
                        case 'ً':
                            $output .= '&#x064B;';
                            break;
                        case 'ٌ':
                            $output .= '&#x064C;';
                            break;
                        case 'ٍ':
                            $output .= '&#x064D;';
                            break;
                        case 'َ':
                            $output .= '&#x064E;';
                            break;
                        case 'ُ':
                            $output .= '&#x064F;';
                            break;
                        case 'ِ':
                            $output .= '&#x0650;';
                            break;
                        case 'ْ':
                            $output .= '&#x0652;';
                            break;
                    }
                }
                continue;
            }

            // check if it should connect to the prev char, then adjust the form value accordingly
            if ($prevChar && isset($this->arGlyphs[$prevChar]) && $this->arGlyphs[$prevChar]['prevLink'] == true) {
                $form++;
            }

            // check if it should connect to the next char, the adjust the form value accordingly
            if ($nextChar && isset($this->arGlyphs[$nextChar]) && $this->arGlyphs[$nextChar]['nextLink'] == true) {
                $form += 2;
            }

            // add the current char UTF-8 code to the output string
            if (isset($this->arGlyphs[$crntChar])) {
                $output .= '&#x' . $this->arGlyphs[$crntChar][$form] . ';';
            } else {
                $output .= $crntChar;
            }

            // next char will be the current one before loop (we are going backword to manage right-to-left presenting)
            $nextChar = $crntChar;
        }

        // from Arabic Presentation Forms-B, Range: FE70-FEFF,
        // file "UFE70.pdf" (in reversed order)
        // into Arabic Presentation Forms-A, Range: FB50-FDFF, file "UFB50.pdf"
        // Example: $output = strtr($output, ['&#xFEA0;&#xFEDF;' => '&#xFCC9;']);
        // Lam Jeem
        $output = $this->arGlyphsDecodeEntities($output, $exclude = ['&']);

        return $output;
    }

    /**
     * Convert Arabic string into glyph joining in UTF-8
     * hexadecimals stream (take care of whole the document including English
     * sections as well as numbers and arcs etc...)
     *
     * @param string  $text      Arabic string
     * @param integer $max_chars Max number of chars you can fit in one line
     * @param boolean $hindo     If true use Hindo digits else use Arabic digits
     * @param boolean $forcertl  If true forces RTL in the bidi algorithm
     *
     * @return string Arabic glyph joining in UTF-8 hexadecimals stream (take
     *                care of whole document including English sections as well
     *                as numbers and arcs etc...)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function utf8Glyphs($text, $max_chars = 50, $hindo = true, $forcertl = false)
    {
        $lines = [];
        $pairs = [];

        $harakat = ['َ', 'ً', 'ُ', 'ٌ', 'ِ', 'ٍ'];
        foreach ($harakat as $haraka) {
            $pairs["ّ$haraka"] = "{$haraka}ّ";
        }

        $text = strtr($text, $pairs);

        // process by line required for bidi in RTL case
        $userLines = explode("\n", $text);

        foreach ($userLines as $line) {
            // wrape long lines for bidi in RTL case
            while (mb_strlen($line) > $max_chars) {
                // find the last space before hit the max line length
                $last = mb_strrpos(mb_substr($line, 0, $max_chars), ' ');

                // add it as a new line in the lines array
                $lines[] = mb_substr($line, 0, $last);

                // the rest of the line will be our new line now to iterate
                $line = mb_substr($line, $last + 1, mb_strlen($line) - $last);
            }

            $lines[] = $line;
        }

        $outLines = [];

        foreach ($lines as $str) {
            // identify Arabic fragments in the line for glyphs
            $p = $this->arIdentify($str);

            // check if current line has any Arabic fragment
            if (count($p) > 0) {
                // rtl if the current line starts by Arabic or the whole text is forced to be rtl
                if ($forcertl == true || $p[0] == 0) {
                    $rtl = true;
                } else {
                    $rtl = false;
                }

                // block structure to save processed fragments
                $block = [];

                // if line does not start by Arabic, then save first non-Arabic fragment in block structure
                if ($p[0] != 0) {
                    $block[] = substr($str, 0, $p[0]);
                }

                // get the last Arabic fragment identifier
                $max = count($p);

                // if the bidi logic is rtl
                if ($rtl == true) {
                    // check the start for each Arabic fragment
                    for ($i = 0; $i < $max; $i += 2) {
                        // alter start position to include the prev. close bracket if exist
                        $p[$i] = strlen(preg_replace('/\)\s*$/', '', substr($str, 0, $p[$i])));
                    }
                }

                // for each Arabic fragment
                for ($i = 0; $i < $max; $i += 2) {
                    // do glyphs pre-processing and save the result in the block structure
                    $block[] = $this->arGlyphsPreConvert(substr($str, $p[$i], $p[$i + 1] - $p[$i]));

                    // if we still have another Arabic fragment
                    if ($i + 2 < $max) {
                        // get the in-between non-Arabic fragment as is and save it in the block structure
                        $block[] = substr($str, $p[$i + 1], $p[$i + 2] - $p[$i + 1]);
                    } elseif ($p[$i + 1] != strlen($str)) {
                        // else, the whole fragment starts after the last Arabic fragment
                        // until the end of the string will be save as is (non-Arabic) in the block structure
                        $block[] = substr($str, $p[$i + 1], strlen($str) - $p[$i + 1]);
                    }
                }

                // if the logic is rtl, then reverse the blocks order before concatenate
                if ($rtl == true) {
                    $block = array_reverse($block);
                }

                // concatenate the whole string blocks
                $str = implode('', $block);
            }

            // add the processed string to the output lines array
            $outLines[] = $str;
        }

        // concatenate the whole text lines using \n
        $output = implode("\n", $outLines);

        // convert to Hindu numerals if requested
        if ($hindo == true) {
            $output = strtr($output, array_combine($this->numeralArabic, $this->numeralHindu));
        }

        return $output;
    }

    /**
     * Decode all HTML entities (including numerical ones) to regular UTF-8 bytes.
     * Double-escaped entities will only be decoded once
     * ("&amp;lt;" becomes "&lt;", not "<").
     *
     * @param string        $text    The text to decode entities in.
     * @param array<string> $exclude An array of characters which should not be decoded.
     *                               For example, ['<', '&', '"']. This affects
     *                               both named and numerical entities.
     *
     * @return string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arGlyphsDecodeEntities($text, $exclude = [])
    {
        // Get all named HTML entities.
        $table = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'UTF-8'));

        // Add apostrophe (XML)
        $table['&apos;'] = "'";

        $newtable = array_diff($table, $exclude);

        // Use a regexp to select all entities in one pass, to avoid decoding double-escaped entities twice.
        $text = preg_replace_callback('/&(#x?)?([A-Fa-f0-9]+);/u', function ($matches) use ($newtable, $exclude) {
            return $this->arGlyphsDecodeEntities2($matches[1], $matches[2], $matches[0], $newtable, $exclude);
        }, $text);

        return $text;
    }

    /**
     * Helper function for decodeEntities
     *
     * @param string        $prefix    Prefix
     * @param string        $codepoint Codepoint
     * @param string        $original  Original
     * @param array<string> $table     Store named entities in a table
     * @param array<string> $exclude   An array of characters which should not be decoded
     *
     * @return string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arGlyphsDecodeEntities2($prefix, $codepoint, $original, &$table, &$exclude)
    {
        // Named entity
        if (!$prefix) {
            if (isset($table[$original])) {
                return $table[$original];
            } else {
                return $original;
            }
        }

        // Hexadecimal numerical entity
        if ($prefix == '#x') {
            $codepoint = base_convert($codepoint, 16, 10);
        }

        $str = '';

        // Encode codepoint as UTF-8 bytes
        if ($codepoint < 0x80) {
            $str = chr((int)$codepoint);
        } elseif ($codepoint < 0x800) {
            $str = chr(0xC0 | ((int)$codepoint >> 6)) . chr(0x80 | ((int)$codepoint & 0x3F));
        } elseif ($codepoint < 0x10000) {
            $str = chr(0xE0 | ((int)$codepoint >> 12)) . chr(0x80 | (((int)$codepoint >> 6) & 0x3F)) .
                   chr(0x80 | ((int)$codepoint & 0x3F));
        } elseif ($codepoint < 0x200000) {
            $str = chr(0xF0 | ((int)$codepoint >> 18)) . chr(0x80 | (((int)$codepoint >> 12) & 0x3F)) .
                   chr(0x80 | (((int)$codepoint >> 6) & 0x3F)) . chr(0x80 | ((int)$codepoint & 0x3F));
        }

        // Check for excluded characters
        if (in_array($str, $exclude, true)) {
            return $original;
        } else {
            return $str;
        }
    }


    /////////////////////////////////////// Query /////////////////////////////////////////////////

    /**
     * Setting value for $_fields array
     *
     * @param array<string> $arrConfig Name of the fields that SQL statement will search
     *                                 them (in array format where items are those fields names)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setQueryArrFields($arrConfig)
    {
        if (is_array($arrConfig)) {
            // Get arQueryFields array
            $this->arQueryFields = $arrConfig;
        }

        return $this;
    }

    /**
     * Setting value for $_fields array
     *
     * @param string $strConfig Name of the fields that SQL statement will search
     *                          them (in string format using comma as delimated)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setQueryStrFields($strConfig)
    {
        if (is_string($strConfig)) {
            // Get arQueryFields array
            $this->arQueryFields = explode(',', $strConfig);
        }

        return $this;
    }

    /**
     * Setting $mode propority value that refer to search mode [0 for OR logic | 1 for AND logic]
     *
     * @param integer $mode Setting value to be saved in the $mode propority [0 for OR logic | 1 for AND logic]
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setQueryMode($mode)
    {
        if (in_array($mode, ['0', '1'])) {
            // Set search mode [0 for OR logic | 1 for AND logic]
            $this->arQueryMode = $mode;
        }

        return $this;
    }

    /**
     * Getting $mode propority value that refer to search mode [0 for OR logic | 1 for AND logic]
     *
     * @return integer Value of $mode properity
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getQueryMode()
    {
        // Get search mode value [0 for OR logic | 1 for AND logic]
        return $this->arQueryMode;
    }

    /**
     * Getting values of $_fields Array in array format
     *
     * @return array<string> Value of $_fields array in Array format
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getQueryArrFields()
    {
        $fields = $this->arQueryFields;

        return $fields;
    }

    /**
     * Getting values of $_fields array in String format (comma delimated)
     *
     * @return string Values of $_fields array in String format (comma delimated)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getQueryStrFields()
    {
        $fields = implode(',', $this->arQueryFields);

        return $fields;
    }

    /**
     * Build WHERE section of the SQL statement using defind lex's rules, search
     * mode [AND | OR], and handle also phrases (inclosed by "") using normal
     * LIKE condition to match it as it is.
     *
     * @param string $arg String that user search for in the database table
     *
     * @return string The WHERE section in SQL statement (MySQL database engine format)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arQueryWhereCondition($arg)
    {
        $sql = '';

        //$arg   = mysql_real_escape_string($arg);
        $search  = ["\\",  "\x00", "\n",  "\r",  "'",  '"', "\x1a"];
        $replace = ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"];
        $arg     = strtr($arg, array_combine($search, $replace));

        // Check if there are phrases in $arg should handle as it is
        $phrase = explode("\"", $arg);

        if (count($phrase) > 2) {
            // Re-init $arg variable
            // (It will contain the rest of $arg except phrases).
            $arg = '';

            for ($i = 0; $i < count($phrase); $i++) {
                $subPhrase = $phrase[$i];
                if ($i % 2 == 0 && $subPhrase != '') {
                    // Re-build $arg variable after restricting phrases
                    $arg .= $subPhrase;
                } elseif ($i % 2 == 1 && $subPhrase != '') {
                    // Handle phrases using reqular LIKE matching in MySQL
                    $wordCondition[] = $this->getWordLike($subPhrase);
                }
            }
        }

        // Handle normal $arg using lex's and regular expresion
        $words = preg_split('/\s+/', trim($arg));

        foreach ($words as $word) {
            //if (is_numeric($word) || strlen($word) > 2) {
                // Take off all the punctuation
                //$word = preg_replace("/\p{P}/", '', $word);
                $exclude = ['(', ')', '[', ']', '{', '}', ',', ';', ':', '?', '!', '،', '؛', '؟'];
                $word    = strtr($word, array_fill_keys($exclude, ''));

                $wordCondition[] = $this->getWordRegExp($word);
            //}
        }

        if (!empty($wordCondition)) {
            if ($this->arQueryMode == 0) {
                $sql = '(' . implode(') OR (', $wordCondition) . ')';
            } elseif ($this->arQueryMode == 1) {
                $sql = '(' . implode(') AND (', $wordCondition) . ')';
            }
        }

        return $sql;
    }

    /**
     * Search condition in SQL format for one word in all defind fields using
     * REGEXP clause and lex's rules
     *
     * @param string $arg String (one word) that you want to build a condition for
     *
     * @return string sub SQL condition (for internal use)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function getWordRegExp($arg)
    {
        $arg = $this->arQueryLex($arg);
        $argLike = preg_replace('/\(([^()]|(?R))*\)\??/', '', $arg);
        $sql = implode(" LIKE '%$argLike%' AND REGEXP '$arg') OR (", $this->arQueryFields) .
               " LIKE '%$argLike%' AND REGEXP '$arg'";
        /*
        $sql = ' REPLACE(' . implode(", 'ـ', '') REGEXP '$arg' OR REPLACE(", $this->arQueryFields) .
               ", 'ـ', '') REGEXP '$arg'";
        */
        return $sql;
    }

    /**
     * Search condition in SQL format for one word in all defind fields using
     * normal LIKE clause
     *
     * @param string $arg String (one word) that you want to build a condition for
     *
     * @return string sub SQL condition (for internal use)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function getWordLike($arg)
    {
        $sql = implode(" LIKE '$arg' OR ", $this->arQueryFields) . " LIKE '$arg'";

        return $sql;
    }

    /**
     * Get more relevant order by section related to the user search keywords
     *
     * @param string $arg String that user search for in the database table
     *
     * @return string sub SQL ORDER BY section
     * @author Saleh AlMatrafe <saleh@saleh.cc>
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arQueryOrderBy($arg)
    {
        $wordOrder = [];

        // Check if there are phrases in $arg should handle as it is
        $phrase = explode("\"", $arg);

        if (count($phrase) > 2) {
            // Re-init $arg variable (It will contain the rest of $arg except phrases).
            $arg = '';
            for ($i = 0; $i < count($phrase); $i++) {
                if ($i % 2 == 0 && isset($phrase[$i])) {
                    // Re-build $arg variable after restricting phrases
                    $arg .= $phrase[$i];
                } elseif ($i % 2 == 1 && isset($phrase[$i])) {
                    // Handle phrases using reqular LIKE matching in MySQL
                    $wordOrder[] = $this->getWordLike($phrase[$i]);
                }
            }
        }

        // Handle normal $arg using lex's and regular expresion
        $words = explode(' ', $arg);
        foreach ($words as $word) {
            if ($word != '') {
                $wordOrder[] = 'CASE WHEN (' . $this->getWordRegExp($word) . ') THEN 1 ELSE 0 END';
            }
        }

        $order = '((' . implode(') + (', $wordOrder) . ')) DESC';

        return $order;
    }

    /**
     * This method will implement various regular expressin rules based on
     * pre-defined Arabic lexical rules
     *
     * @param string $arg String of one word user want to search for
     *
     * @return string Regular Expression format to be used in MySQL query statement
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arQueryLex($arg)
    {
        $this->arQueryInit();

        $arg = preg_replace($this->arQueryLexPatterns, $this->arQueryLexReplacements, $arg);

        return $arg;
    }

    /**
     * Get most possible Arabic lexical forms for a given word
     *
     * @param string $word String that user search for
     *
     * @return array<string> list of most possible Arabic lexical forms for a given word
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arQueryAllWordForms($word)
    {
        $wordForms = [$word];

        $postfix1 = ['كم', 'كن', 'نا', 'ها', 'هم', 'هن'];
        $postfix2 = ['ين', 'ون', 'ان', 'ات', 'وا'];

        if (mb_substr($word, 0, 2) == 'ال') {
            $word = mb_substr($word, 2, mb_strlen($word));
        }

        $len = mb_strlen($word);

        $wordForms[] = $word;

        $str1 = mb_substr($word, 0, -1);
        $str2 = mb_substr($word, 0, -2);
        $str3 = mb_substr($word, 0, -3);

        $last1 = mb_substr($word, -1, $len);
        $last2 = mb_substr($word, -2, $len);
        $last3 = mb_substr($word, -3, $len);

        if ($len >= 6 && $last3 == 'تين') {
            $wordForms[] = $str3;
            $wordForms[] = $str3 . 'ة';
            $wordForms[] = $word . 'ة';
        }

        if ($len >= 6 && ($last3 == 'كما' || $last3 == 'هما')) {
            $wordForms[] = $str3;
            $wordForms[] = $str3 . 'كما';
            $wordForms[] = $str3 . 'هما';
        }

        if ($len >= 5 && in_array($last2, $postfix2)) {
            $wordForms[] = $str2;
            $wordForms[] = $str2 . 'ة';
            $wordForms[] = $str2 . 'تين';

            foreach ($postfix2 as $postfix) {
                $wordForms[] = $str2 . $postfix;
            }
        }

        if ($len >= 5 && in_array($last2, $postfix1)) {
            $wordForms[] = $str2;
            $wordForms[] = $str2 . 'ي';
            $wordForms[] = $str2 . 'ك';
            $wordForms[] = $str2 . 'كما';
            $wordForms[] = $str2 . 'هما';

            foreach ($postfix1 as $postfix) {
                $wordForms[] = $str2 . $postfix;
            }
        }

        if ($len >= 5 && $last2 == 'ية') {
            $wordForms[] = $str1;
            $wordForms[] = $str2;
        }

        if (
            ($len >= 4 && ($last1 == 'ة' || $last1 == 'ه' || $last1 == 'ت'))
            || ($len >= 5 && $last2 == 'ات')
        ) {
            $wordForms[] = $str1;
            $wordForms[] = $str1 . 'ة';
            $wordForms[] = $str1 . 'ه';
            $wordForms[] = $str1 . 'ت';
            $wordForms[] = $str1 . 'ات';
        }

        if ($len >= 4 && $last1 == 'ى') {
            $wordForms[] = $str1 . 'ا';
        }

        if (preg_match("/(\\S{1,})ئ(\\S{1,})/", $word) != false) {
            foreach ($wordForms as $form) {
                $wordForms[] = preg_replace("/(\\S{1,})ئ(\\S{1,})/", "\\1ي\\2", $form);
            }
        }

        $trans = ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا'];
        foreach ($wordForms as $form) {
            $normWord = strtr($form, $trans);
            if ($normWord != $form) {
                $wordForms[] = $normWord;
            }
        }

        $wordForms = array_unique($wordForms);

        return $wordForms;
    }

    /**
     * Get most possible Arabic lexical forms of user search keywords
     *
     * @param string $arg String that user search for
     *
     * @return string list of most possible Arabic lexical forms for given keywords
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arQueryAllForms($arg)
    {
        $wordForms = [];
        $words     = explode(' ', $arg);

        foreach ($words as $word) {
            $wordForms = array_merge($wordForms, $this->arQueryAllWordForms($word));
        }

        $str = implode(' ', $wordForms);

        return $str;
    }


    /////////////////////////////////////// Salat /////////////////////////////////////////////////

    /**
     * Setting date of day for Salat calculation
     *
     * @param integer $m Month of date you want to calculate Salat in
     * @param integer $d Day of date you want to calculate Salat in
     * @param integer $y Year (four digits) of date you want to calculate Salat in
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSalatDate($m = 8, $d = 2, $y = 1975)
    {
        if (is_numeric($y) && $y > 0 && $y < 3000) {
            $this->salatYear = (int)floor($y);
        }

        if (is_numeric($m) && $m >= 1 && $m <= 12) {
            $this->salatMonth = (int)floor($m);
        }

        if (is_numeric($d) && $d >= 1 && $d <= 31) {
            $this->salatDay = (int)floor($d);
        }

        return $this;
    }

    /**
     * Setting location information for Salat calculation
     *
     * @param float   $l1 Latitude of location you want to calculate Salat time in
     * @param float   $l2 Longitude of location you want to calculate Salat time in
     * @param integer $z  Time Zone, offset from UTC (see also Greenwich Mean Time)
     * @param integer $e  Elevation, it is the observer's height in meters.
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSalatLocation($l1 = 36.20278, $l2 = 37.15861, $z = 2, $e = 0)
    {
        if (is_numeric($l1) && $l1 >= -180 && $l1 <= 180) {
            $this->salatLat = $l1;
        }

        if (is_numeric($l2) && $l2 >= -180 && $l2 <= 180) {
            $this->salatLong = $l2;
        }

        if (is_numeric($z) && $z >= -12 && $z <= 12) {
            $this->salatZone = (int)floor($z);
        }

        if (is_numeric($e)) {
            $this->salatElevation = $e;
        }

        return $this;
    }

    /**
     * Setting rest of Salat calculation configuration
     *
     * Convention [Fajr Angle, Isha Angle]
     * - Muslim World League [-18, -17]
     * - Islamic Society of North America (ISNA) [-15, -15]
     * - Egyptian General Authority of Survey [-19.5, -17.5]
     * - Umm al-Qura University, Makkah [-18.5, Isha 90  min after Maghrib, 120 min during Ramadan]
     * - University of Islamic Sciences, Karachi [-18, -18]
     * - Institute of Geophysics, University of Tehran [-17.7, -14*]
     * - Shia Ithna Ashari, Leva Research Institute, Qum [-16, -14]
     *
     * (*) Isha angle is not explicitly defined in Tehran method
     * Fajr Angle = $fajrArc, Isha Angle = $ishaArc
     *
     * @param string $sch        [Shafi|Hanafi] to define Muslims Salat calculation method (affect Asr time)
     * @param float  $sunriseArc Sun rise arc (default value is -0.833333)
     * @param float  $ishaArc    Isha arc (default value is -18)
     * @param float  $fajrArc    Fajr arc (default value is -18)
     * @param string $view       [Sunni|Shia] to define Muslims Salat calculation method
     *                            (affect Maghrib and Midnight time)
     *
     * @return object $this to build a fluent interface
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSalatConf(
        $sch = 'Shafi',
        $sunriseArc = -0.833333,
        $ishaArc = -17.5,
        $fajrArc = -19.5,
        $view = 'Sunni'
    ) {
        $sch = ucfirst($sch);

        if ($sch == 'Shafi' || $sch == 'Hanafi') {
            $this->salatSchool = $sch;
        }

        if (is_numeric($sunriseArc) && $sunriseArc >= -180 && $sunriseArc <= 180) {
            $this->salatAB2 = $sunriseArc;
        }

        if (is_numeric($ishaArc) && $ishaArc >= -180 && $ishaArc <= 180) {
            $this->salatAG2 = $ishaArc;
        }

        if (is_numeric($fajrArc) && $fajrArc >= -180 && $fajrArc <= 180) {
            $this->salatAJ2 = $fajrArc;
        }

        if ($view == 'Sunni' || $view == 'Shia') {
            $this->salatView = $view;
        }

        return $this;
    }

    /**
     * Calculate Salat times for the date set in setSalatDate methode, and
     * location set in setSalatLocation.
     *
     * @return array<int, array<int, int>|float|string>
     *                       of Salat times + sun rise in the following format
     *                       hh:mm where hh is the hour in local format and 24 mode
     *                       mm is minutes with leading zero to be 2 digits always
     *                       array items is [$Fajr, $Sunrise, $Dhuhr, $Asr, $Maghrib,
     *                       $Isha, $Sunset, $Midnight, $Imsak, array $timestamps]
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     * @author Hamid Zarrabi-Zadeh <zarrabi@scs.carleton.ca>
     * @source http://praytimes.org/calculation
     */
    public function getPrayTime()
    {
        $unixtimestamp = mktime(0, 0, 0, $this->salatMonth, $this->salatDay, $this->salatYear);

        // Calculate Julian date
        if ($this->salatMonth <= 2) {
            $year  = $this->salatYear - 1;
            $month = $this->salatMonth + 12;
        } else {
            $year  = $this->salatYear;
            $month = $this->salatMonth;
        }

        $A = floor($year / 100);
        $B = 2 - $A + floor($A / 4);

        $jd = floor(365.25 * ($year + 4716)) + floor(30.6001 * ($month + 1)) + $this->salatDay + $B - 1524.5;

        // The following algorithm from U.S. Naval Observatory computes the
        // Sun's angular coordinates to an accuracy of about 1 arcminute within
        // two centuries of 2000.
        $d = $jd - 2451545.0;  // jd is the given Julian date

        // The following algorithm from U.S. Naval Observatory computes the Sun's
        // angular coordinates to an accuracy of about 1 arcminute within two
        // centuries of 2000
        // http://aa.usno.navy.mil/faq/docs/SunApprox.php
        // Note: mod % in PHP ignore decimels!
        $g = 357.529 + 0.98560028 * $d;
        $g = fmod($g, 360) + $g - ceil($g) + 1;

        $q = 280.459 + 0.98564736 * $d;
        $q = fmod($q, 360) + $q - ceil($q) + 1;

        $L = $q + 1.915 * sin(deg2rad($g)) + 0.020 * sin(deg2rad(2 * $g));
        $L = fmod($L, 360) + $L - ceil($L) + 1;

        $R = 1.00014 - 0.01671 * cos(deg2rad($g)) - 0.00014 * cos(deg2rad(2 * $g));
        $e = 23.439 - 0.00000036 * $d;

        $RA = rad2deg(atan2(cos(deg2rad($e)) * sin(deg2rad($L)), cos(deg2rad($L)))) / 15;

        if ($RA < 0) {
            $RA = 24 + $RA;
        }

        // The declination of the Sun is the angle between the rays of the sun and
        // the plane of the earth equator. The declination of the Sun changes
        // continuously throughout the year. This is a consequence of the Earth's
        // tilt, i.e. the difference in its rotational and revolutionary axes.
        // declination of the Sun
        $D = rad2deg(asin(sin(deg2rad($e)) * sin(deg2rad($L))));

        // The equation of time is the difference between time as read from sundial
        // and a clock. It results from an apparent irregular movement of the Sun
        // caused by a combination of the obliquity of the Earth's rotation axis
        // and the eccentricity of its orbit. The sundial can be ahead (fast) by
        // as much as 16 min 33 s (around November 3) or fall behind by as much as
        // 14 min 6 s (around February 12), as shown in the following graph:
        // http://en.wikipedia.org/wiki/File:Equation_of_time.png
        $EqT = ($q / 15) - $RA;  // equation of time

        // Dhuhr
        // When the Sun begins to decline after reaching its highest point in the sky
        $Dhuhr = 12 + $this->salatZone - ($this->salatLong / 15) - $EqT;
        if ($Dhuhr < 0) {
            $Dhuhr = 24 + $Dhuhr;
        }

        // Sunrise & Sunset
        // If the observer's location is higher than the surrounding terrain, we
        // can consider this elevation into consideration by increasing the above
        // constant 0.833 by 0.0347 × sqrt(elevation), where elevation is the
        // observer's height in meters.
        $alpha = -1 * $this->salatAB2 + 0.0347 * sqrt($this->salatElevation);
        $n     = -1 * sin(deg2rad($alpha)) - sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
        $d     = cos(deg2rad($this->salatLat)) * cos(deg2rad($D));

        // date_sun_info Returns an array with information about sunset/sunrise
        // and twilight begin/end
        $Sunrise = $Dhuhr - (1 / 15) * rad2deg(acos($n / $d));
        $Sunset  = $Dhuhr + (1 / 15) * rad2deg(acos($n / $d));

        // Fajr & Isha
        // Imsak: The time to stop eating Sahur (for fasting), slightly before Fajr.
        // Fajr:  When the sky begins to lighten (dawn).
        // Isha:  The time at which darkness falls and there is no scattered light
        //        in the sky.
        $n     = -1 * sin(deg2rad(abs($this->salatAJ2))) - sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
        $Fajr  = $Dhuhr - (1 / 15) * rad2deg(acos($n / $d));
        $Imsak = $Fajr - (10 / 60);

        $n     = -1 * sin(deg2rad(abs($this->salatAG2))) - sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
        $Isha  = $Dhuhr + (1 / 15) * rad2deg(acos($n / $d));

        // Asr
        // The following formula computes the time difference between the mid-day
        // and the time at which the object's shadow equals t times the length of
        // the object itself plus the length of that object's shadow at noon
        if ($this->salatSchool == 'Shafi') {
            $n = sin(atan(1 / (1 + tan(deg2rad($this->salatLat - $D))))) -
                 sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
        } else {
            $n = sin(atan(1 / (2 + tan(deg2rad($this->salatLat - $D))))) -
                 sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
        }

        $Asr = $Dhuhr + (1 / 15) * rad2deg(acos($n / $d));

        // Maghrib
        if ($this->salatView == 'Sunni') {
            // In the Sunni's point of view, the time for Maghrib prayer begins once
            // the Sun has completely set beneath the horizon, that is, Maghrib = Sunset
            // (some calculators suggest 1 to 3 minutes after Sunset for precaution)
            $Maghrib = $Sunset + 2 / 60;
        } else {
            // In the Shia's view, however, the dominant opinion is that as long as
            // the redness in the eastern sky appearing after sunset has not passed
            // overhead, Maghrib prayer should not be performed.
            $n       = -1 * sin(deg2rad(4)) - sin(deg2rad($this->salatLat)) * sin(deg2rad($D));
            $Maghrib = $Dhuhr + (1 / 15) * rad2deg(acos($n / $d));
        }

        // Midnight
        if ($this->salatView == 'Sunni') {
            // Midnight is generally calculated as the mean time from Sunset to Sunrise
            $Midnight = $Sunset + 0.5 * ($Sunrise - $Sunset);
        } else {
            // In Shia point of view, the juridical midnight (the ending time for
            // performing Isha prayer) is the mean time from Sunset to Fajr
            $Midnight = $Sunset + 0.5 * ($Fajr - $Sunset);
        }

        if ($Midnight > 12) {
            $Midnight = $Midnight - 12;
        } else {
            $Midnight = $Midnight + 12;
        }

        // Result.ThlthAkhir:= Result.Fajr - (24 - Result.Maghrib + Result.Fajr) / 3;
        // Result.Doha      := Result.Sunrise + (15 / 60);
        // if isRamadan then (Um-Al-Qura calendar)
        // Result.Isha      := Result.Maghrib + 2
        // else Result.Isha := Result.Maghrib + 1.5;

        $times = [$Fajr, $Sunrise, $Dhuhr, $Asr, $Maghrib, $Isha, $Sunset, $Midnight, $Imsak];

        // Convert number after the decimal point into minutes
        foreach ($times as $index => $time) {
            $hours   = (int)floor($time);
            $minutes = round(($time - $hours) * 60);

            if ($minutes < 10) {
                $minutes = "0$minutes";
            }

            $times[$index] = "$hours:$minutes";

            $times[9][$index] = (int)($unixtimestamp + 3600 * $hours + 60 * $minutes);

            if ($index == 7 && $hours < 6) {
                $times[9][$index] += 24 * 3600;
            }
        }

        return $times;
    }

    /**
     * Determine Qibla direction using basic spherical trigonometric formula
     *
     * @return float Qibla Direction (from the north direction) in degrees
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     * @author S. Kamal Abdali <k.abdali@acm.org>
     * @source http://www.patriot.net/users/abdali/ftp/qibla.pdf
     */
    public function getQibla()
    {
        // The geographical coordinates of the Ka'ba
        $K_latitude  = 21.423333;
        $K_longitude = 39.823333;

        $latitude  = $this->salatLat;
        $longitude = $this->salatLong;

        $numerator   = sin(deg2rad($K_longitude - $longitude));
        $denominator = (cos(deg2rad($latitude)) * tan(deg2rad($K_latitude))) -
                       (sin(deg2rad($latitude)) * cos(deg2rad($K_longitude - $longitude)));

        $q = atan($numerator / $denominator);
        $q = rad2deg($q);

        if ($this->salatLat > 21.423333) {
            $q += 180;
        }

        return $q;
    }

    /**
     * Convert coordinates presented in degrees, minutes and seconds
     * (e.g. 12°34'56"S formula) into usual float number in degree unit scale
     * (e.g. -12.5822 value)
     *
     * @param string $value Coordinate presented in degrees, minutes and seconds (e.g. 12°34'56"S formula)
     *
     * @return float Equivalent float number in degree unit scale (e.g. -12.5822 value)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function dms2dd($value)
    {
        $pattern = "/(\d{1,2})°((\d{1,2})')?((\d{1,2})\")?([NSEW])/i";

        preg_match($pattern, $value, $matches);

        $degree = (int)$matches[1] + ((int)$matches[3] / 60) + ((float)$matches[5] / 3600);

        $direction = strtoupper($matches[6]);

        if ($direction == 'S' || $direction == 'W') {
            $degree = -1 * $degree;
        }

        return $degree;
    }

    /**
     * Convert coordinates presented in float number in degree unit scale
     * (e.g. -12.5822 value) into degrees, minutes and seconds (e.g. -12°34'56" formula)
     *
     * @param float $value Coordinate presented in float number in degree unit scale (e.g. -12.5822 value)
     *
     * @return string Equivalent coordinate presented in degrees, minutes and seconds (e.g. -12°34'56" formula)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function dd2dms($value)
    {
        if ($value < 0) {
            $value = abs($value);
            $dd    = '-';
        } else {
            $dd = '';
        }

        $degrees = (int)$value;
        $minutes = (int)(($value - $degrees) * 60);
        $seconds = round(((($value - $degrees) * 60) - $minutes) * 60, 4);

        if ($degrees > 0) {
            $dd .= $degrees . '°';
        }

        if ($minutes >= 10) {
            $dd .= $minutes . '\'';
        } else {
            $dd .= '0' . $minutes . '\'';
        }

        if ($seconds >= 10) {
            $dd .= $seconds . '"';
        } else {
            $dd .= '0' . $seconds . '"';
        }

        return $dd;
    }


    /////////////////////////////////////// Summary ///////////////////////////////////////////////

    /**
     * Load enhanced Arabic stop words list
     *
     * @return void
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arSummaryLoadExtra()
    {
        $this->arSummaryInit();

        $extra_words = file($this->rootDirectory . '/data/ar_stopwords_extra.txt');
        $extra_words = array_map('trim', $extra_words);

        $this->arSummaryCommonWords = array_merge($this->arSummaryCommonWords, $extra_words);
    }

    /**
     * Core summarize function that implement required steps in the algorithm
     *
     * @param string  $str      Input Arabic document as a string
     * @param string  $keywords List of keywords higlited by search process
     * @param integer $int      Sentences value (see $mode effect also)
     * @param integer $mode     Mode of sentences count [1|2] for "number" and "rate" modes respectively
     * @param integer $output   Output mode [1|2] for "summary" and "highlight" modes respectively
     *
     * @return string Output summary requested
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arSummary($str, $keywords, $int, $mode, $output)
    {
        $this->arSummaryInit();

        preg_match_all("/[^\.\n\،\؛\,\;](.+?)[\.\n\،\؛\,\;]/u", $str, $sentences);
        $_sentences = $sentences[0];

        if ($mode == 1) {
            $str            = preg_replace("/\s{2,}/u", ' ', $str);
            $totalChars     = mb_strlen($str);
            $totalSentences = count($_sentences);

            $maxChars = round($int * $totalChars / 100);
            $int      = round($int * $totalSentences / 100);
        } else {
            $maxChars = 99999;
        }

        $summary = '';

        $str           = strip_tags($str);
        $normalizedStr = $this->arNormalize($str);
        $cleanedStr    = $this->arCleanCommon($normalizedStr);
        $stemStr       = $this->arDraftStem($cleanedStr);

        preg_match_all("/[^\.\n\،\؛\,\;](.+?)[\.\n\،\؛\,\;]/u", $stemStr, $sentences);
        $_stemmedSentences = $sentences[0];

        $wordRanks = $this->arSummaryRankWords($stemStr);

        if ($keywords) {
            $keywords = $this->arNormalize($keywords);
            $keywords = $this->arDraftStem($keywords);
            $words    = explode(' ', $keywords);

            foreach ($words as $word) {
                $wordRanks[$word] = 1000;
            }
        }

        $sentencesRanks          = $this->arSummaryRankSentences($_sentences, $_stemmedSentences, $wordRanks);
        list($sentences, $ranks) = $sentencesRanks;
        $minRank                 = $this->arSummaryMinAcceptedRank($sentences, $ranks, $int, $maxChars);
        $totalSentences          = count($ranks);

        for ($i = 0; $i < $totalSentences; $i++) {
            if ($sentencesRanks[1][$i] >= $minRank) {
                if ($output == 1) {
                    $summary .= ' ' . $sentencesRanks[0][$i];
                } else {
                    $summary .= '<mark>' . $sentencesRanks[0][$i] . '</mark>';
                }
            } else {
                if ($output == 2) {
                    $summary .= $sentencesRanks[0][$i];
                }
            }
        }

        if ($output == 2) {
            $summary = strtr($summary, ["\n" => '<br />']);
        }

        $summary = trim(preg_replace('/\s+/', ' ', $summary));

        return $summary;
    }

    /**
     * Extract keywords from a given Arabic string (document content)
     *
     * @param string  $str Input Arabic document as a string
     * @param integer $int Number of keywords required to be extracting from input string (document content)
     *
     * @return string List of the keywords extracting from input Arabic string (document content)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arSummaryKeywords($str, $int)
    {
        $patterns     = [];
        $replacements = [];
        $metaKeywords = '';

        $patterns[]     = '/\.|\n|\،|\؛|\(|\[|\{|\)|\]|\}|\,|\;/u';
        $replacements[] = ' ';

        $str = preg_replace($patterns, $replacements, $str);

        $normalizedStr = $this->arNormalize($str);
        $cleanedStr    = $this->arCleanCommon($normalizedStr);

        $str = preg_replace('/(\W)ال(\w{3,})/u', '\\1\\2', $cleanedStr);
        $str = preg_replace('/(\W)وال(\w{3,})/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})هما(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})كما(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})تين(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})هم(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})هن(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ها(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})نا(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ني(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})كم(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})تم(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})كن(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ات(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ين(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})تن(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ون(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ان(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})تا(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})وا(\W)/u', '\\1\\2', $str);
        $str = preg_replace('/(\w{3,})ة(\W)/u', '\\1\\2', $str);

        $stemStr = preg_replace('/(\W)\w{1,3}(\W)/u', '\\2', $str);

        $wordRanks = $this->arSummaryRankWords($stemStr);

        arsort($wordRanks, SORT_NUMERIC);

        $i = 1;
        foreach ($wordRanks as $key => $value) {
            if ($this->arSummaryAcceptedWord($key)) {
                $metaKeywords .= $key . '، ';
                $i++;
            }
            if ($i > $int) {
                break;
            }
        }

        $metaKeywords = mb_substr($metaKeywords, 0, -2);

        return $metaKeywords;
    }

    /**
     * Normalized Arabic document
     *
     * @param string $str Input Arabic document as a string
     *
     * @return string Normalized Arabic document
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arNormalize($str)
    {
        $str = strtr($str, array_fill_keys($this->arNormalizeAlef, 'ا'));
        $str = strtr($str, array_fill_keys($this->arNormalizeDiacritics, ''));
        $str = strtr($str, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');

        return $str;
    }

    /**
     * Extracting common Arabic words (roughly)
     * from input Arabic string (document content)
     *
     * @param string $str Input normalized Arabic document as a string
     *
     * @return string Arabic document as a string free of common words (roughly)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arCleanCommon($str)
    {
        $this->arSummaryInit();

        $str = str_replace(' ', '  ', $str);
        $str = strtr(" $str", array_fill_keys($this->arSummaryCommonWords, ' '));
        $str = str_replace('  ', ' ', $str);

        return trim($str);
    }

    /**
     * Remove less significant Arabic letter from given string (document content).
     * Please note that output will not be human readable.
     *
     * @param string $str Input Arabic document as a string
     *
     * @return string Output string after removing less significant Arabic letter (not human readable output)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arDraftStem($str)
    {
        $str = strtr($str, array_fill_keys($this->arCommonChars, ''));

        return $str;
    }

    /**
     * Ranks words in a given Arabic string (document content). That rank refers
     * to the frequency of that word appears in that given document.
     *
     * @param string $str Input Arabic document as a string
     *
     * @return array<int> Associated array where document words referred by index and
     *                    those words ranks referred by values of those array items.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSummaryRankWords($str)
    {
        $wordsRanks = [];

        $str   = strtr($str, array_fill_keys($this->arSeparators, ' '));
        $words = preg_split("/[\s,]+/u", $str);

        foreach ($words as $word) {
            if (isset($wordsRanks[$word])) {
                $wordsRanks[$word]++;
            } else {
                $wordsRanks[$word] = 1;
            }
        }

        return $wordsRanks;
    }

    /**
     * Ranks sentences in a given Arabic string (document content).
     *
     * @param array<string> $sentences        Sentences of the input Arabic document as an array
     * @param array<string> $stemmedSentences Stemmed sentences of the input Arabic document as an array
     * @param array<int>    $arr              Words ranks array (word as an index and value refer to the word frequency)
     *
     * @return array<int, array<int, float|int|string>> Two dimension array, first item is an array of document
     *                                        sentences, second item is an array of ranks of document sentences.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSummaryRankSentences($sentences, $stemmedSentences, $arr)
    {
        $this->arSummaryInit();

        $sentenceArr = [];
        $rankArr     = [];
        $importent   = implode('|', $this->arSummaryImportantWords);

        $max = count($sentences);

        for ($i = 0; $i < $max; $i++) {
            $sentence = $sentences[$i];

            $w     = 0;
            $first = mb_substr($sentence, 0, 1);
            $last  = mb_substr($sentence, -1, 1);

            if ($first == "\n") {
                $w += 3;
            } elseif (in_array($first, $this->arSeparators, true)) {
                $w += 2;
            } else {
                $w += 1;
            }

            if ($last == "\n") {
                $w += 3;
            } elseif (in_array($last, $this->arSeparators, true)) {
                $w += 2;
            } else {
                $w += 1;
            }

            preg_match_all('/(' . $importent . ')/', $sentence, $out);
            $w += count($out[0]);

            $_sentence = mb_substr($sentence, 0, -1);
            $sentence  = mb_substr($_sentence, 1, mb_strlen($_sentence));

            if (!in_array($first, $this->arSeparators, true)) {
                $sentence = $first . $sentence;
            }

            $stemStr = $stemmedSentences[$i];
            $stemStr = mb_substr($stemStr, 0, -1);

            $words = preg_split("/[\s,]+/u", $stemStr);

            $totalWords = count($words);
            if ($totalWords > 4) {
                $totalWordsRank = 0;

                foreach ($words as $word) {
                    if (isset($arr[$word])) {
                        $totalWordsRank += $arr[$word];
                    }
                }

                $wordsRank     = $totalWordsRank / $totalWords;
                $sentenceRanks = $w * $wordsRank;

                $sentenceArr[] = $sentence . $last;
                $rankArr[]     = $sentenceRanks;
            }
        }

        $sentencesRanks = [$sentenceArr, $rankArr];

        return $sentencesRanks;
    }

    /**
     * Calculate minimum rank for sentences which will be including in the summary
     *
     * @param array<string> $str Document sentences
     * @param array<int>    $arr Sentences ranks
     * @param integer $int  Number of sentences you need to include in your summary
     * @param integer $max  Maximum number of characters accepted in your summary
     *
     * @return integer Minimum accepted sentence rank (sentences with rank more
     *                 than this will be listed in the document summary)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSummaryMinAcceptedRank($str, $arr, $int, $max)
    {
        $len = [];

        foreach ($str as $line) {
            $len[] = mb_strlen($line);
        }

        rsort($arr, SORT_NUMERIC);

        $totalChars = 0;
        $minRank = 0;

        for ($i = 0; $i <= $int; $i++) {
            if (!isset($arr[$i])) {
                $minRank = 0;
                break;
            }

            $totalChars += $len[$i];

            if ($totalChars >= $max) {
                $minRank = $arr[$i];
                break;
            }

            $minRank = $arr[$i];
        }

        return $minRank;
    }

    /**
     * Check some conditions to know if a given string is a formal valid word or not
     *
     * @param string $word String to be checked if it is a valid word or not
     *
     * @return boolean True if passed string is accepted as a valid word else it will return False
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSummaryAcceptedWord($word)
    {
        $accept = true;

        if (mb_strlen($word) < 3) {
            $accept = false;
        }

        return $accept;
    }

    /////////////////////////////////////// Identifier ///////////////////////////////////////////////

    /**
     * Identify Arabic text in a given UTF-8 multi language string
     *
     * @param string  $str  UTF-8 multi language string
     * @param boolean $html If True, then ignore the HTML tags (default is TRUE)
     *
     * @return array<int> Offset of the beginning and end of each Arabic segment in
     *                    sequence in the given UTF-8 multi language string
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arIdentify($str, $html = true)
    {
        // https://utf8-chartable.de/unicode-utf8-table.pl?start=1536&number=128&utf8=dec
        $minAr    = 55424;
        $maxAr    = 55743;

        $probAr   = false;
        $arFlag   = false;
        $htmlFlag = false;
        $arRef    = [];
        $max      = strlen($str);
        $ascii    = unpack('C*', $str);

        $i = -1;
        while (++$i < $max) {
            $cDec = $ascii[$i + 1];

            if ($html == true) {
                if ($cDec == 60 && $ascii[$i + 2] != 32) {
                    $htmlFlag = true;
                } elseif ($htmlFlag == true && $cDec == 62) {
                    $htmlFlag = false;
                } elseif ($htmlFlag == true) {
                    continue;
                }
            }

            // ignore ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 :
            // If it come in the Arabic context
            if ($cDec >= 33 && $cDec <= 58) {
                continue;
            }

            if (!$probAr && ($cDec == 216 || $cDec == 217)) {
                $probAr = true;
                continue;
            }

            if ($i > 0) {
                $pDec = $ascii[$i];
            } else {
                $pDec = null;
            }

            if ($probAr) {
                $utfDecCode = ($pDec << 8) + $cDec;
                if ($utfDecCode >= $minAr && $utfDecCode <= $maxAr) {
                    if (!$arFlag) {
                        $arFlag  = true;
                        // include the previous open bracket ( if it is exists
                        $sp = strlen(rtrim(substr($str, 0, $i - 1))) - 1;
                        if ($str[$sp] == '(') {
                            $arRef[] = $sp;
                        } else {
                            $arRef[] = $i - 1;
                        }
                    }
                } else {
                    if ($arFlag) {
                        $arFlag  = false;
                        $arRef[] = $i - 1;
                    }
                }

                $probAr = false;
                continue;
            }

            if ($arFlag && !preg_match("/^\s$/", $str[$i])) {
                $arFlag  = false;
                // tag out the trailer spaces
                $sp = $i - strlen(rtrim(substr($str, 0, $i)));
                $arRef[] = $i - $sp;
            }
        }

        if ($arFlag) {
            $arRef[] = $i;
        }

        return $arRef;
    }

    /**
     * Find out if given string is Arabic text or not
     *
     * @param string $str String
     *
     * @return boolean True if given string is UTF-8 Arabic, else will return False
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function isArabic($str)
    {
        $val = false;
        $arr = $this->arIdentify($str);

        if (count($arr) == 2 && $arr[0] == 0 && $arr[1] == strlen($str)) {
            $val = true;
        }

        return $val;
    }

    /**
     * Encode a location coordinates (latitude and longitude in WGS84) into Open Location Code
     * Ref: https://github.com/google/open-location-code/blob/master/docs/specification.md
     *
     * @param float   $latitude   Coordinate presented in float number in degree unit scale (e.g. 34.67175)
     * @param float   $longitude  Coordinate presented in float number in degree unit scale (e.g. 36.263625)
     * @param integer $codeLength Code length, default value is 10 (this provides an area that is
     *                            1/8000 x 1/8000 degree in size, roughly 14x14 meters)
     *
     * @return string Open Location Code string (e.g. 8G6RM7C7+PF)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function dd2olc($latitude, $longitude, $codeLength = 10)
    {
        $codeLength = $codeLength / 2;
        $validChars = '23456789CFGHJMPQRVWX';

        $latitude  = $latitude + 90;
        $longitude = $longitude + 180;

        $latitude  = round($latitude * pow(20, $codeLength - 2), 0);
        $longitude = round($longitude * pow(20, $codeLength - 2), 0);

        $olc = '';

        for ($i = 1; $i <= $codeLength; $i++) {
            $x = $longitude % 20;
            $y = $latitude % 20;

            $longitude = floor($longitude / 20);
            $latitude  = floor($latitude / 20);

            $olc = substr($validChars, $y, 1) . substr($validChars, $x, 1) . $olc;

            if ($i == 1) {
                $olc = '+' . $olc;
            }
        }

        return $olc;
    }

    /**
     * Decode an Open Location Code string into its location coordinates in decimal degrees.
     * Ref: https://github.com/google/open-location-code/blob/master/docs/specification.md
     *
     * @param string  $olc        Open Location Code string (e.g. 8G6RM7C7+PF)
     * @param integer $codeLength Code length, default value is 10 (this provides an area that is
     *                            1/8000 x 1/8000 degree in size, roughly 14x14 meters)
     *
     * @return array<null|float>  Location coordinates in decimal degrees [latitude, longitude] in WGS84
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function olc2dd($olc, $codeLength = 10)
    {
        $coordinates = [];

        if ($this->volc($olc, $codeLength)) {
            $codeLength = $codeLength / 2;
            $validChars = '23456789CFGHJMPQRVWX';

            $olc = strtoupper(strtr($olc, ['+' => '']));

            $latitude  = 0;
            $longitude = 0;

            for ($i = 1; $i <= $codeLength; $i++) {
                $latitude  = $latitude + strpos($validChars, substr($olc, 2 * $i - 2, 1)) * pow(20, 2 - $i);
                $longitude = $longitude + strpos($validChars, substr($olc, 2 * $i - 1, 1)) * pow(20, 2 - $i);
            }

            $coordinates[] = $latitude - 90;
            $coordinates[] = $longitude - 180;
        } else {
            $coordinates[] = null;
            $coordinates[] = null;
        }

        return $coordinates;
    }

    /**
     * Determine if an Open Location Code is valid.
     * Ref: https://github.com/google/open-location-code/blob/master/docs/specification.md
     *
     * @param string  $olc        Open Location Code string (e.g. 8G6RM7C7+PF)
     * @param integer $codeLength Code length, default value is 10 (this provides an area that is
     *                            1/8000 x 1/8000 degree in size, roughly 14x14 meters)
     *
     * @return boolean String represents a valid Open Location Code.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function volc($olc, $codeLength = 10)
    {
        if (strlen($olc) != $codeLength + 1) {
            $isValid = false;
        } elseif (substr($olc, -3, 1) != '+') {
            $isValid = false;
        } elseif (preg_match('/[^2-9CFGHJMPQRVWX+]/', strtoupper($olc))) {
            $isValid = false;
        } else {
            $isValid = true;
        }

        return $isValid;
    }

    /**
     * Get proper Arabic plural form
     * There are 4 plural forms in Arabic language:
     * - Form for 2
     * - Form for numbers that end with a number between 3 and 10 (like: 103, 1405, 23409)
     * - Form for numbers that end with a number between 11 and 99 (like: 1099, 278)
     * - Form for numbers above 100 ending with 0, 1 or 2 (like: 100, 232, 3001)
     *
     * @param string  $singular Singular word (e.g., عنصر).
     * @param integer $count    The number (e.g. item count) to determine the proper plural form.
     * @param string  $plural2  Plural form 2 (e.g., عنصران). If NULL [default] retrive from internal JSON dataset.
     * @param string  $plural3  Plural form 3 (e.g., عناصر). If NULL [default] retrive from internal JSON dataset.
     * @param string  $plural4  Plural form 4 (e.g., عنصرا). If NULL [default] retrive from internal JSON dataset.
     * @param bool    $nameOnly Get only the name without the number, e.g., if you want to style numbers separately.
     * @param bool    $isFemale Is Female.
     *
     * @return string Proper plural form of the given singular form
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arPlural(
        $singular,
        $count,
        $plural2 = null,
        $plural3 = null,
        $plural4 = null,
        $nameOnly = false,
        $isFemale = null
    ) {
        $this->arPluralInit();

        $isFemale = $isFemale === null ? $this->isFemale($singular) : $isFemale;

        if ($count == 0) {
            # $plural = $this->arPluralsForms[$singular][0] ?? "لا $plural3";
            if (isset($this->arPluralsForms[$singular][0])) {
                $plural = $this->arPluralsForms[$singular][0];
            } else {
                $plural = "لا $plural3";
            }
        } elseif ($count == 1 && $isFemale) {
            # $plural = $this->arPluralsForms[$singular][1] ?? "$singular واحدة";
            if (isset($this->arPluralsForms[$singular][1])) {
                $plural = $this->arPluralsForms[$singular][1];
            } else {
                $plural = "$singular واحدة";
            }
        } elseif ($count == 1 && !$isFemale) {
            # $plural = $this->arPluralsForms[$singular][1] ?? "$singular واحد";
            if (isset($this->arPluralsForms[$singular][1])) {
                $plural = $this->arPluralsForms[$singular][1];
            } else {
                $plural = "$singular واحد";
            }
        } elseif ($count == 2) {
            # $plural = $this->arPluralsForms[$singular][2] ?? $plural2;
            if (isset($this->arPluralsForms[$singular][2])) {
                $plural = $this->arPluralsForms[$singular][2];
            } else {
                $plural = $plural2;
            }
        } elseif ($count % 100 >= 3 && $count % 100 <= 10) {
            # $plural = $this->arPluralsForms[$singular][3] ?? "%d $plural3";
            if (isset($this->arPluralsForms[$singular][3])) {
                $plural = $this->arPluralsForms[$singular][3];
            } else {
                $plural = "%d $plural3";
            }
        } elseif ($count % 100 >= 11) {
            # $plural = $this->arPluralsForms[$singular][4] ?? "%d $plural4";
            if (isset($this->arPluralsForms[$singular][4])) {
                $plural = $this->arPluralsForms[$singular][4];
            } else {
                $plural = "%d $plural4";
            }
        } else {
            # $plural = $this->arPluralsForms[$singular][5] ?? "%d $singular";
            if (isset($this->arPluralsForms[$singular][5])) {
                $plural = $this->arPluralsForms[$singular][5];
            } else {
                $plural = "%d $singular";
            }
        }

        if ($nameOnly) {
            $plural = trim(str_replace('%d', '', $plural));
        }

        return $plural;
    }

    /**
     * Strip Harakat
     *
     * @param string  $text    Arabic text you would like to strip Harakat from it.
     * @param boolean $tatweel Strip Tatweel (default is TRUE).
     * @param boolean $tanwen  Strip Tanwen (default is TRUE).
     * @param boolean $shadda  Strip Shadda (default is TRUE).
     * @param boolean $last    Strip last Harakat (default is TRUE).
     * @param boolean $harakat Strip in word Harakat (default is TRUE).
     *
     * @return string Arabic string clean from selected Harakat
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function stripHarakat($text, $tatweel = true, $tanwen = true, $shadda = true, $last = true, $harakat = true)
    {
        $text = $this->setNorm('stripTatweel', $tatweel)
                     ->setNorm('stripTanween', $tanwen)
                     ->setNorm('stripShadda', $shadda)
                     ->setNorm('stripLastHarakat', $last)
                     ->setNorm('stripWordHarakat', $harakat)
                     ->setNorm('normaliseLamAlef', true)
                     ->setNorm('normaliseAlef', false)
                     ->setNorm('normaliseHamza', false)
                     ->setNorm('normaliseTaa', false)
                     ->arNormalizeText($text);

        return $text;
    }

    /**
     * Arabic Sentiment Analysis
     *
     * @param string $text Arabic review string
     *
     * @return array<boolean|float> of 2 elements: boolean isPositive (negative if false),
     *                              and float probability (range from 0 to 1)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arSentiment($text)
    {
        $this->arSentimentInit();

        # remove mentions
        $text = preg_replace('/@\\S+/u', '', $text);

        # remove hashtags
        $text = preg_replace('/#\\S+/u', '', $text);

        # normalise Alef, Hamza, and Taa
        $text = $this->setNorm('normaliseAlef', true)
                     ->setNorm('normaliseHamza', true)
                     ->setNorm('normaliseTaa', true)
                     ->arNormalizeText($text);

        # filter only Arabic text (white list)
        $text = preg_replace('/[^ ءابتثجحخدذرزسشصضطظعغفقكلمنهوي]+/u', ' ', $text);

        # exclude one letter words
        $text = preg_replace('/\\b\\S{1}\\b/u', ' ', $text);

        # remove extra spaces
        $text = preg_replace('/\\s{2,}/u', ' ', $text);
        $text = preg_replace('/^\\s+/u', '', $text);
        $text = preg_replace('/\\s+$/u', '', $text);

        # split string to words
        $words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);

        # set initial scores
        $score = 0;

        # add a simple rule-based mechanism to handle the negation words
        $negationWords = ['لا', 'ليس', 'غير', 'ما', 'لم', 'لن',
                               'لست', 'ليست', 'ليسا', 'ليستا', 'لستما',
                               'لسنا', 'لستم', 'ليسوا', 'لسن', 'لستن'];
        $negationFlag  = false;

        # for each word
        foreach ($words as $word) {
            # split word to letters
            $letters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);

            $stems = [];

            $n = count($letters);

            # get all possible 2 letters stems of current word
            for ($i = 0; $i < $n - 1; $i++) {
                for ($j = $i + 1; $j < $n; $j++) {
                    # get stem key
                    $stems[] = array_search($letters[$i] . $letters[$j], $this->allStems);
                }
            }

            $log_odds = [];

            # get log odd for all word stems
            foreach ($stems as $key) {
                $log_odds[] = $this->logOddStem[$key];
            }

            # select the most probable stem for current word
            $sel_stem = $stems[array_search(min($log_odds), $log_odds)];

            if ($negationFlag) {
                // switch positive/negative sentiment because of negation word effect
                $score += -1 * (float)$this->logOdd[$sel_stem];

                $negationFlag = false;
            } else {
                # retrive the positive and negative log odd scores and accumulate them
                $score += $this->logOdd[$sel_stem];
            }

            if (in_array($word, $negationWords)) {
                $negationFlag = true;
            }
        }

        if ($score > 0) {
            $isPositive  = true;
        } else {
            $isPositive = false;
        }

        $probability = exp(abs($score)) / (1 + exp(abs($score)));

        return ['isPositive' => $isPositive, 'probability' => $probability];
    }


    /**
     * Arabic Dialects Detector
     *
     * @param string $text Arabic review string
     *
     * @return array<string|float> of 2 elements: string dialect (Egyptian, Levantine, Maghrebi, Peninsular),
     *                             and float probability (range from 0 to 1)
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arDialect($text)
    {
        $this->arDialectInit();

        # remove mentions
        $text = preg_replace('/@\\S+/u', '', $text);

        # remove hashtags
        $text = preg_replace('/#\\S+/u', '', $text);

        # normalise Alef, Hamza, and Taa
        $text = $this->setNorm('normaliseAlef', true)
                     ->setNorm('normaliseHamza', true)
                     ->setNorm('normaliseTaa', true)
                     ->arNormalizeText($text);

        # filter only Arabic text (white list)
        $text = preg_replace('/[^ ءابتثجحخدذرزسشصضطظعغفقكلمنهوي]+/u', ' ', $text);

        # exclude one letter words
        $text = preg_replace('/\\b\\S{1}\\b/u', ' ', $text);

        # remove extra spaces
        $text = preg_replace('/\\s{2,}/u', ' ', $text);
        $text = preg_replace('/^\\s+/u', '', $text);
        $text = preg_replace('/\\s+$/u', '', $text);

        # split string to words
        $words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);

        # set initial scores
        $scoreEgyptian   = 0;
        $scoreLevantine  = 0;
        $scoreMaghrebi   = 0;
        $scorePeninsular = 0;

        # for each word
        foreach ($words as $word) {
            # split word to letters
            $letters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);

            $stems = [];

            $n = count($letters);

            # get all possible 2 letters stems of current word
            for ($i = 0; $i < $n - 1; $i++) {
                for ($j = $i + 1; $j < $n; $j++) {
                    # get stem key
                    $stems[] = array_search($letters[$i] . $letters[$j], $this->dialectsStems);
                }
            }

            $log_odds = [];

            $egp_scores = 0;
            $lev_scores = 0;
            $mag_scores = 0;
            $pen_scores = 0;

            # get log odd scores for all word stems
            foreach ($stems as $key) {
                $log_odds[] = $this->logOddDialects[$key];

                $egp_scores += $this->logOddEgyptian[$key];
                $lev_scores += $this->logOddLevantine[$key];
                $mag_scores += $this->logOddMaghrebi[$key];
                $pen_scores += $this->logOddPeninsular[$key];
            }

            # retrive the positive and negative log odd scores and accumulate them
            $scoreEgyptian   += $egp_scores / count($stems);
            $scoreLevantine  += $lev_scores / count($stems);
            $scoreMaghrebi   += $mag_scores / count($stems);
            $scorePeninsular += $pen_scores / count($stems);
        }

        $score   = max($scoreEgyptian, $scoreLevantine, $scoreMaghrebi, $scorePeninsular);
        $dialect = 'Modern';

        switch ($score) {
            case $scoreEgyptian:
                $dialect = 'Egyptian';
                break;
            case $scoreLevantine:
                $dialect = 'Levantine';
                break;
            case $scoreMaghrebi:
                $dialect = 'Maghrebi';
                break;
            case $scorePeninsular:
                $dialect = 'Peninsular';
                break;
        }

        $probability = exp(abs($score)) / (1 + exp(abs($score)));

        return ['dialect' => $dialect, 'probability' => $probability];
    }


    /**
     * Strip Dots and Hamzat
     *
     * @param string $text Arabic text you would like to strip Dots and Hamzat from it.
     *
     * @return string Arabic text written using letters without dots and Hamzat
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function noDots($text)
    {
        $text = preg_replace('/ن(\b)/u', 'ں$1', $text);
        $text = preg_replace('/ك(\b)/u', 'ک$1', $text);

        $text = strtr($text, ['ب' => 'ٮ', 'ت' => 'ٮ', 'ث' => 'ٮ', 'ن' => 'ٮ',
                                   'ي' => 'ى', 'ف' => 'ڡ', 'ق' => 'ٯ', 'ش' => 'س',
                                   'غ' => 'ع', 'ذ' => 'د', 'ز' => 'ر', 'ض' => 'ص',
                                   'ظ' => 'ط', 'ة' => 'ه', 'ج' => 'ح', 'خ' => 'ح',
                                   'أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا', 'ؤ' => 'و',
                                   'ئ' => 'ى']);

        return $text;
    }

    /////////////////////////////////////// Normalize ///////////////////////////////////////////////

    /**
     * Set given normalization form status.
     *
     * @param string  $form   One of the normalization forms ['stripTatweel', 'stripTanween', 'stripShadda',
     *                        'stripLastHarakat', 'stripWordHarakat', 'normaliseLamAlef',
     *                        'normaliseAlef', 'normaliseHamza', 'normaliseTaa', 'all']
     * @param boolean $status Normalization form status [true|false]
     *
     * @return object $this to build a fluent interface.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setNorm($form, $status)
    {
        if ($status == true) {
            $status = true;
        } else {
            $status = false;
        }

        switch ($form) {
            case 'stripTatweel':
                $this->stripTatweel = $status;
                break;
            case 'stripTanween':
                $this->stripTanween = $status;
                break;
            case 'stripShadda':
                $this->stripShadda = $status;
                break;
            case 'stripLastHarakat':
                $this->stripLastHarakat = $status;
                break;
            case 'stripWordHarakat':
                $this->stripWordHarakat = $status;
                break;
            case 'normaliseLamAlef':
                $this->normaliseLamAlef = $status;
                break;
            case 'normaliseAlef':
                $this->normaliseAlef = $status;
                break;
            case 'normaliseHamza':
                $this->normaliseHamza = $status;
                break;
            case 'normaliseTaa':
                $this->normaliseTaa = $status;
                break;
            case 'all':
                $this->stripTatweel     = $status;
                $this->stripTanween     = $status;
                $this->stripShadda      = $status;
                $this->stripLastHarakat = $status;
                $this->stripWordHarakat = $status;
                $this->normaliseLamAlef = $status;
                $this->normaliseAlef    = $status;
                $this->normaliseHamza   = $status;
                $this->normaliseTaa     = $status;
                break;
        }

        return $this;
    }

    /**
     * Get given normalization form status.
     *
     * @param string  $form One of the normalization forms ['stripTatweel', 'stripTanween', 'stripShadda',
     *                      'stripLastHarakat', 'stripWordHarakat', 'normaliseLamAlef',
     *                      'normaliseAlef', 'normaliseHamza', 'normaliseTaa']
     *
     * @return boolean Selected normalization form status.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getNorm($form)
    {
        switch ($form) {
            case 'stripTatweel':
                $value = $this->stripTatweel;
                break;
            case 'stripTanween':
                $value = $this->stripTanween;
                break;
            case 'stripShadda':
                $value = $this->stripShadda;
                break;
            case 'stripLastHarakat':
                $value = $this->stripLastHarakat;
                break;
            case 'stripWordHarakat':
                $value = $this->stripWordHarakat;
                break;
            case 'normaliseLamAlef':
                $value = $this->normaliseLamAlef;
                break;
            case 'normaliseAlef':
                $value = $this->normaliseAlef;
                break;
            case 'normaliseHamza':
                $value = $this->normaliseHamza;
                break;
            case 'normaliseTaa':
                $value = $this->normaliseTaa;
                break;
            default:
                $value = false;
        }

        return $value;
    }

    /**
     * Normalizes the input provided and returns the normalized string.
     *
     * @param string $text    The input string to normalize.
     * @param string $numeral Symbols used to represent numerical digits [Arabic, Hindu, or Persian]
     *                        default is null (i.e., will not normalize digits in the given string).
     *
     * @return string The normalized string.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function arNormalizeText($text, $numeral = null)
    {
        if ($this->stripWordHarakat) {
            $bodyHarakat = ['/َ(\S)/u', '/ُ(\S)/u', '/ِ(\S)/u', '/ْ(\S)/u'];
            $text = preg_replace($bodyHarakat, '\\1', $text);
        }

        if ($this->stripLastHarakat) {
            $lastHarakat = ['/َ(\s)/u', '/ُ(\s)/u', '/ِ(\s)/u', '/ْ(\s)/u', '/[َُِْ]$/u'];
            $text = preg_replace($lastHarakat, '\\1', $text);
        }

        if ($this->stripTatweel) {
            $text = strtr($text, ['ـ' => '']);
        }

        if ($this->stripTanween) {
            $allTanwen = ['ً' => '', 'ٍ' => '', 'ٌ' => ''];
            $text = strtr($text, $allTanwen);
        }

        if ($this->stripShadda) {
            $text = strtr($text, ['ّ' => '']);
        }

        if ($this->normaliseLamAlef) {
            $search  = ['لا', 'لآ', 'لأ', 'لإ'];
            $replace = ['لا', 'لآ', 'لأ', 'لإ'];
            $text    = str_replace($search, $replace, $text);
        }

        if ($this->normaliseAlef) {
            $text = strtr($text, ['أ' => 'ا', 'إ' => 'ا', 'آ' => 'ا', 'ى' => 'ا']);
        }

        if ($this->normaliseHamza) {
            $text = strtr($text, ['ؤ' => 'ء', 'ئ' => 'ء']);
        }

        if ($this->normaliseTaa) {
            $text = strtr($text, ['ة' => 'ه']);
        }

        if ($numeral == 'Hindu') {
            $text = strtr($text, array_combine($this->numeralPersian, $this->numeralHindu));
            $text = strtr($text, array_combine($this->numeralArabic, $this->numeralHindu));
        } elseif ($numeral == 'Persian') {
            $text = strtr($text, array_combine($this->numeralHindu, $this->numeralPersian));
            $text = strtr($text, array_combine($this->numeralArabic, $this->numeralPersian));
        } elseif ($numeral == 'Arabic') {
            $text = strtr($text, array_combine($this->numeralHindu, $this->numeralArabic));
            $text = strtr($text, array_combine($this->numeralPersian, $this->numeralArabic));
        }

        return($text);
    }

    /**
     * Get the difference in a human readable format.
     *
     * @param int      $time   the timestamp that is being compared.
     * @param int|null $others if null passed, now will be used as comparison reference;
     *                         if integer value, it will be used as reference timestamp.
     *                         (default value is null).
     * @param int      $parts  maximum number of parts to display (default value is 2).
     * @param bool     $floor  logic for rounding last part, if true then use floor, else use ceiling.
     *
     * @return string the difference in a human readable format.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function diffForHumans($time, $others = null, $parts = 2, $floor = true)
    {
        $diff = $others == null ? $time - time() : $time - $others;

        if ($diff < 0) {
            $when = $others == null ? 'منذ' : 'قبل';
        } else {
            $when = $others == null ? 'باقي' : 'بعد';
        }

        $diff = abs($diff);

        $string = '';
        $minute = 60;
        $hour   = 60 * $minute;
        $day    = 24 * $hour;
        $week   = 7 * $day;
        $month  = 30 * $day;
        $year   = 365 * $day;

        while ($parts > 0) {
            if ($diff >= $year) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $year);
                } else {
                    $value = ceil($diff / $year);
                }

                $text = $this->arPlural('سنة', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $year;
                $parts = --$parts;
            } elseif ($diff >= $month) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $month);
                } else {
                    $value = ceil($diff / $month);
                }

                $text = $this->arPlural('شهر', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $month;
                $parts = --$parts;
            } elseif ($diff >= $week) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $week);
                } else {
                    $value = ceil($diff / $week);
                }

                $text = $this->arPlural('إسبوع', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $week;
                $parts = --$parts;
            } elseif ($diff >= $day) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $day);
                } else {
                    $value = ceil($diff / $day);
                }

                $text = $this->arPlural('يوم', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $day;
                $parts = --$parts;
            } elseif ($diff >= $hour) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $hour);
                } else {
                    $value = ceil($diff / $hour);
                }

                $text = $this->arPlural('ساعة', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $hour;
                $parts = --$parts;
            } elseif ($diff >= $minute) {
                if ($parts > 1 || $floor === true) {
                    $value = floor($diff / $minute);
                } else {
                    $value = ceil($diff / $minute);
                }

                $text = $this->arPlural('دقيقة', (int)$value);
                $text = str_replace('%d', (string)$value, $text);

                $string = $string == '' ? $text : $string . ' و ' . $text;

                $diff  = $diff % $minute;
                $parts = --$parts;
            } else {
                if ($diff > 0) {
                    $text = $this->arPlural('ثانية', (int)$diff);
                    $text = str_replace('%d', (string)$diff, $text);

                    $string = $string == '' ? $text : $string . ' و ' . $text;
                }

                $parts = 0;
            }
        }

        $string = $when . ' ' . $string;

        return $string;
    }

    /////////////////////////////////////// Similarity ////////////////////////////////////////////

    /**
     * Calculates the keyboard proximity similarity score between two characters.
     *
     * This method initializes similarity parameters and computes the similarity score of two characters
     * based on their positions on a keyboard. The score considers the key positions (row, column) and
     * the shift key status, with the following conditions:
     * - `1` if the characters are the same key (with a penalty for differing shift status).
     * - `0.5` for space bar proximity.
     * - `0.5` if the characters are adjacent horizontally (with a penalty for differing shift status).
     * - `0.25` if the characters are adjacent vertically or diagonally (with a penalty for differing shift status).
     * - `0` for all other cases.
     *
     * @param string $chr1 The first character to compare.
     * @param string $chr2 The second character to compare.
     *
     * @return float The keyboard similarity score between the two characters (ranging from 0 to 1).
     */
    private function arKeyboardSimilarity($chr1, $chr2)
    {
        $this->arSimilarityInit();

        // key order in the row (left to right)
        $xi = (int)$this->arKeyX["$chr1"];
        $xj = (int)$this->arKeyX["$chr2"];

        // key row (buttom to up)
        $yi = (int)$this->arKeyY["$chr1"];
        $yj = (int)$this->arKeyY["$chr2"];

        // shift key status (0/1 if pressed)
        $zi = (int)$this->arKeyZ["$chr1"];
        $zj = (int)$this->arKeyZ["$chr2"];

        // similarity score
        $score = 0;

        if ($yi == $yj && $xi == $xj) {
            // the same key + shift status penalty if differ
            $score = 1 - 0.5 * abs($zi - $zj);
        } elseif (min($yi, $yj) == 0 && max($xi, $xj) > 1 && max($xi, $xj) < 9) {
            // space bar case
            $score = 0.5;
        } elseif ($yi == $yj && abs($xi - $xj) == 1) {
            // left or right + shift status penalty if differ
            $score = 0.5 - 0.25 * abs($zi - $zj);
        } elseif (abs($yi - $yj) == 1 && ($xi - $xj == 0 || $xi - $xj == $yi - $yj)) {
            // up or down + shift status penalty if differ
            $score = 0.25 - 0.125 * abs($zi - $zj);
        }

        return $score;
    }

    /**
     * Calculates the graphical similarity score between two characters.
     *
     * This method initializes similarity parameters and determines the graphical similarity score
     * of two characters based on their membership in predefined graphical groups. The score is:
     * - `1` if the characters are identical.
     * - `0.5` if the characters belong to the same graphical group.
     * - `0` if they do not belong to the same graphical group or are not defined in the graphical groups.
     *
     * @param string $chr1 The first character to compare.
     * @param string $chr2 The second character to compare.
     *
     * @return float The graphical similarity score between the two characters (0, 0.5, or 1).
     */
    private function arGraphicSimilarity($chr1, $chr2)
    {
        $this->arSimilarityInit();

        if (!array_key_exists($chr1, $this->arGraphGroup) || !array_key_exists($chr2, $this->arGraphGroup)) {
            $score = 0;
        } else {
            $chr1Group = $this->arGraphGroup["$chr1"];
            $chr2Group = $this->arGraphGroup["$chr2"];

            if ($chr1 == $chr2) {
                $score = 1;
            } elseif ($chr1Group == $chr2Group) {
                $score = 0.5;
            } else {
                $score = 0;
            }
        }

        return $score;
    }

    /**
     * Calculates the phonetic similarity score between two characters.
     *
     * This method initializes similarity parameters and determines the phonetic similarity score
     * of two characters based on their membership in predefined sound groups. The score is:
     * - `1` if the characters are identical.
     * - `0.5` if the characters belong to the same sound group.
     * - `0` if they do not belong to the same sound group or are not defined in the sound groups.
     *
     * @param string $chr1 The first character to compare.
     * @param string $chr2 The second character to compare.
     *
     * @return float The phonetic similarity score between the two characters (0, 0.5, or 1).
     */
    private function arSoundSimilarity($chr1, $chr2)
    {
        $this->arSimilarityInit();

        if ($chr1 == $chr2) {
            $score = 1;
        } elseif (!array_key_exists($chr1, $this->arSoundGroup) || !array_key_exists($chr2, $this->arSoundGroup)) {
            $score = 0;
        } else {
            $chr1Group = $this->arSoundGroup["$chr1"];
            $chr2Group = $this->arSoundGroup["$chr2"];

            if ($chr1Group == $chr2Group) {
                $score = 0.5;
            } else {
                $score = 0;
            }
        }

        return $score;
    }

    /**
     * Calculates the similarity score between two characters.
     *
     * This method computes the similarity score of two characters based on keyboard proximity,
     * graphical similarity, and phonetic similarity. The final score is a weighted average of the
     * individual similarity scores using predefined weights for each factor.
     *
     * @param string $chr1 The first character to compare.
     * @param string $chr2 The second character to compare.
     *
     * @return float The calculated similarity score between the two characters as a weighted average.
     */
    private function s($chr1, $chr2)
    {
        $totalWeight = $this->keyboardWeight + $this->graphicWeight + $this->phoneticWeight;

        $score = $this->arKeyboardSimilarity($chr1, $chr2) * $this->keyboardWeight / $totalWeight;
        $score += $this->arGraphicSimilarity($chr1, $chr2) * $this->graphicWeight / $totalWeight;
        $score += $this->arSoundSimilarity($chr1, $chr2) * $this->phoneticWeight / $totalWeight;

        return $score;
    }

    /**
     * Calculates the gap penalty for a given character.
     *
     * This method initializes similarity parameters, checks if the given character exists in the
     * predefined gap penalty array, and calculates the penalty score. If the character is not found,
     * a default penalty score of 1 is used.
     *
     * @param string $chr The character for which the gap penalty is calculated.
     *
     * @return float The calculated gap penalty as a negative integer.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function d($chr)
    {
        $this->arSimilarityInit();

        if (array_key_exists($chr, $this->arGapPenalty)) {
            $score = $this->arGapPenalty["$chr"];
        } else {
            $score = 1;
        }

        return -1 * $score;
    }

    /**
     * Needleman-Wunsch algorithm using weighted scoring matrices and gap penalty
     * https://en.wikipedia.org/wiki/Needleman-Wunsch_algorithm
     *
     * @param string $string1 The first string.
     * @param string $string2 The second string.
     *
     * @return float Similarity score.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    private function arSimilarityScore($string1, $string2)
    {
        $max1 = mb_strlen($string1);
        $max2 = mb_strlen($string2);

        $F = [];

        $F[0][0] = 0;

        for ($i = 1; $i <= $max1; $i++) {
            $chr = mb_substr($string1, $i - 1, 1);
            $F[$i][0] = $this->d($chr) + $F[$i - 1][0];
        }

        for ($j = 1; $j <= $max2; $j++) {
            $chr = mb_substr($string2, $j - 1, 1);
            $F[0][$j] = $this->d($chr) + $F[0][$j - 1];
        }

        for ($i = 1; $i <= $max1; $i++) {
            for ($j = 1; $j <= $max2; $j++) {
                $A = mb_substr($string1, $i - 1, 1);
                $B = mb_substr($string2, $j - 1, 1);

                $match  = $F[$i - 1][$j - 1] + $this->s($A, $B);
                $delete = $F[$i - 1][$j] + $this->d($A);
                $insert = $F[$i][$j - 1] + $this->d($B);

                $F[$i][$j] = max($match, $delete, $insert);
            }
        }
        $score   = $F[$max1][$max2];

        return $score;
    }

    /**
     * Calculate the similarity between two Arabic strings
     *
     * @param string $string1  The first string.
     * @param string $string2  The second string.
     * @param float|null &$percent [optional] By passing a reference as the third argument, this will be filled
     *                              with the percentage of similarity between the two strings.
     *
     * @return float The number of matching chars in both strings.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function similar_text($string1, $string2, &$percent = null)
    {
        $score  = $this->arSimilarityScore($string1, $string2);
        $score1 = $this->arSimilarityScore($string1, $string1);
        $score2 = $this->arSimilarityScore($string2, $string2);

        if ($percent !== null) {
            $percent = 100 * $score / max($score1, $score2);
        }

        return $score;
    }

    /**
     * Set Similarity Weight
     *
     * @param string $source [keyboardWeight|graphicWeight|phoneticWeight]
     * @param float  $value
     *
     * @return object $this to build a fluent interface.
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function setSimilarityWeight($source, $value = 1)
    {
        switch ($source) {
            case 'keyboardWeight':
                $this->keyboardWeight = $value;
                break;
            case 'graphicWeight':
                $this->graphicWeight = $value;
                break;
            case 'phoneticWeight':
                $this->phoneticWeight = $value;
                break;
        }

        return $this;
    }

    /**
     * Get Similarity Weight
     *
     * @param string $source [keyboardWeight|graphicWeight|phoneticWeight]
     *
     * @return float the similarity weight for the given source
     * @author Khaled Al-Sham'aa <khaled@ar-php.org>
     */
    public function getSimilarityWeight($source)
    {
        switch ($source) {
            case 'keyboardWeight':
                $value = $this->keyboardWeight;
                break;
            case 'graphicWeight':
                $value = $this->graphicWeight;
                break;
            case 'phoneticWeight':
                $value = $this->phoneticWeight;
                break;
            default:
                $value = 1;
        }

        return $value;
    }
    /*
    public function getSoundSimilarityGroups()
    {
        // check line 800
        foreach($this->arSoundGroup as $char => $value) $groups[$value][] = $char;

        return $this->arSoundGroup;
    }
    */
}
