<?php

namespace DynamicOOOS\Mpdf\Shaper;

use DynamicOOOS\Mpdf\Ucdn;
class Indic
{
    /* FROM hb-ot-shape-complex-indic-private.hh */
    // indic_category
    const OT_X = 0;
    const OT_C = 1;
    const OT_V = 2;
    const OT_N = 3;
    const OT_H = 4;
    const OT_ZWNJ = 5;
    const OT_ZWJ = 6;
    const OT_M = 7;
    /* Matra or Dependent Vowel */
    const OT_SM = 8;
    const OT_VD = 9;
    const OT_A = 10;
    const OT_NBSP = 11;
    const OT_DOTTEDCIRCLE = 12;
    /* Not in the spec, but special in Uniscribe. /Very very/ special! */
    const OT_RS = 13;
    /* Register Shifter, used in Khmer OT spec */
    const OT_COENG = 14;
    const OT_REPHA = 15;
    const OT_RA = 16;
    /* Not explicitly listed in the OT spec, but used in the grammar. */
    const OT_CM = 17;
    /* Visual positions in a syllable from left to right. */
    /* FROM hb-ot-shape-complex-indic-private.hh */
    // indic_position
    const POS_START = 0;
    const POS_RA_TO_BECOME_REPH = 1;
    const POS_PRE_M = 2;
    const POS_PRE_C = 3;
    const POS_BASE_C = 4;
    const POS_AFTER_MAIN = 5;
    const POS_ABOVE_C = 6;
    const POS_BEFORE_SUB = 7;
    const POS_BELOW_C = 8;
    const POS_AFTER_SUB = 9;
    const POS_BEFORE_POST = 10;
    const POS_POST_C = 11;
    const POS_AFTER_POST = 12;
    const POS_FINAL_C = 13;
    const POS_SMVD = 14;
    const POS_END = 15;
    /*
     * Basic features.
     * These features are applied in order, one at a time, after initial_reordering.
     */
    /*
     * Must be in the same order as the indic_features array. Ones starting with _ are F_GLOBAL
     * Ones without the _ are only applied where the mask says!
     */
    const _NUKT = 0;
    const _AKHN = 1;
    const RPHF = 2;
    const _RKRF = 3;
    const PREF = 4;
    const BLWF = 5;
    const HALF = 6;
    const ABVF = 7;
    const PSTF = 8;
    const CFAR = 9;
    // Khmer only
    const _VATU = 10;
    const _CJCT = 11;
    const INIT = 12;
    // Based on indic_category used to make string to find syllables
    // OT_ to string character (using e.g. OT_C from INDIC) hb-ot-shape-complex-indic-private.hh
    public static $indic_category_char = [
        'x',
        'C',
        'V',
        'N',
        'H',
        'Z',
        'J',
        'M',
        'S',
        'v',
        'A',
        /* Spec gives Andutta U+0952 as OT_A. However, testing shows that Uniscribe
         * treats U+0951..U+0952 all as OT_VD - see set_indic_properties */
        's',
        'D',
        'F',
        /* Register shift Khmer only */
        'G',
        /* Khmer only */
        'r',
        /* 0D4E (dot reph) only one in Malayalam */
        'R',
        'm',
    ];
    public static function set_indic_properties(&$info, $scriptblock)
    {
        $u = $info['uni'];
        $type = self::indic_get_categories($u);
        $cat = $type & 0x7f;
        $pos = $type >> 8;
        /*
         * Re-assign category
         */
        if ($u == 0x17d1) {
            $cat = self::OT_X;
        }
        if ($cat == self::OT_X && self::in_range($u, 0x17cb, 0x17d3)) {
            /* Khmer Various signs */
            /* These are like Top Matras. */
            $cat = self::OT_M;
            $pos = self::POS_ABOVE_C;
        }
        if ($u == 0x17c6) {
            $cat = self::OT_N;
        }
        /* Khmer Bindu doesn't like to be repositioned. */
        if ($u == 0x17d2) {
            $cat = self::OT_COENG;
        }
        /* Khmer coeng */
        /* The spec says U+0952 is OT_A.	However, testing shows that Uniscribe
         * treats U+0951..U+0952 all as OT_VD.
         * TESTS:
         * U+092E,U+0947,U+0952
         * U+092E,U+0952,U+0947
         * U+092E,U+0947,U+0951
         * U+092E,U+0951,U+0947
         * */
        //if ($u == 0x0952) $cat = self::OT_A;
        if (self::in_range($u, 0x951, 0x954)) {
            $cat = self::OT_VD;
        }
        if ($u == 0x200c) {
            $cat = self::OT_ZWNJ;
        } elseif ($u == 0x200d) {
            $cat = self::OT_ZWJ;
        } elseif ($u == 0x25cc) {
            $cat = self::OT_DOTTEDCIRCLE;
        } elseif ($u == 0xa71) {
            $cat = self::OT_SM;
        }
        /* GURMUKHI ADDAK.	More like consonant medial. like 0A75. */
        if ($cat == self::OT_REPHA) {
            /* There are two kinds of characters marked as Repha:
             * - The ones that are GenCat=Mn are already positioned visually, ie. after base. (eg. Khmer)
             * - The ones that are GenCat=Lo is encoded logically, ie. beginning of syllable. (eg. Malayalam)
             *
             * We recategorize the first kind to look like a Nukta and attached to the base directly.
             */
            if ($info['general_category'] == Ucdn::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) {
                $cat = self::OT_N;
            }
        }
        /*
         * Re-assign position.
         */
        if (self::FLAG($cat) & (self::FLAG(self::OT_C) | self::FLAG(self::OT_CM) | self::FLAG(self::OT_RA) | self::FLAG(self::OT_V) | self::FLAG(self::OT_NBSP) | self::FLAG(self::OT_DOTTEDCIRCLE))) {
            // = CONSONANT_FLAGS like is_consonant
            if ($scriptblock == Ucdn::SCRIPT_KHMER) {
                $pos = self::POS_BELOW_C;
            } else {
                $pos = self::POS_BASE_C;
            }
            /* Will recategorize later based on font lookups. */
            if (self::is_ra($u)) {
                $cat = self::OT_RA;
            }
        } elseif ($cat == self::OT_M) {
            $pos = self::matra_position($u, $pos);
        } elseif ($cat == self::OT_SM || $cat == self::OT_VD) {
            $pos = self::POS_SMVD;
        }
        if ($u == 0xb01) {
            $pos = self::POS_BEFORE_SUB;
        }
        /* Oriya Bindu is BeforeSub in the spec. */
        $info['indic_category'] = $cat;
        $info['indic_position'] = $pos;
    }
    // syllable_type
    const CONSONANT_SYLLABLE = 0;
    const VOWEL_SYLLABLE = 1;
    const STANDALONE_CLUSTER = 2;
    const BROKEN_CLUSTER = 3;
    const NON_INDIC_CLUSTER = 4;
    public static function set_syllables(&$o, $s, &$broken_syllables)
    {
        $ptr = 0;
        $syllable_serial = 1;
        $broken_syllables = \false;
        while ($ptr < \strlen($s)) {
            $match = '';
            $syllable_length = 1;
            $syllable_type = self::NON_INDIC_CLUSTER;
            // CONSONANT_SYLLABLE Consonant syllable
            // From OT spec:
            if (\preg_match('/^([CR]m*[N]?(H[ZJ]?|[ZJ]H))*[CR]m*[N]?[A]?(H[ZJ]?|[M]*[N]?[H]?)?[S]?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                // From HarfBuzz:
                //if (preg_match('/^r?([CR]J?(Z?[N]{0,2})?[ZJ]?H(J[N]?)?){0,4}[CR]J?(Z?[N]{0,2})?A?((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::CONSONANT_SYLLABLE;
            } elseif (\preg_match('/^(RH|r)?V[N]?([ZJ]?H[CR]m*|J[CR]m*)?([M]*[N]?[H]?)?[S]?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                // From HarfBuzz:
                //else if (preg_match('/^(RH|r)?V(Z?[N]{0,2})?(J|([ZJ]?H(J[N]?)?[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2})/', substr($s,$ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::VOWEL_SYLLABLE;
            } elseif (($ptr == 0 || $o[$ptr - 1]['general_category'] < Ucdn::UNICODE_GENERAL_CATEGORY_LOWERCASE_LETTER || $o[$ptr - 1]['general_category'] > Ucdn::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) && \preg_match('/^(RH|r)?[sD][N]?([ZJ]?H[CR]m*)?([M]*[N]?[H]?)?[S]?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                // From HarfBuzz:
                // && (preg_match('/^(RH|r)?[sD](Z?[N]{0,2})?(([ZJ]?H(J[N]?)?)[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::STANDALONE_CLUSTER;
            } elseif (\preg_match('/^(RH|r)?[N]?([ZJ]?H[CR])?([M]*[N]?[H]?)?[S]?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                // From HarfBuzz:
                //else if (preg_match('/^(RH|r)?(Z?[N]{0,2})?(([ZJ]?H(J[N]?)?)[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
                if (\strlen($ma[0])) {
                    // May match blank
                    $syllable_length = \strlen($ma[0]);
                    $syllable_type = self::BROKEN_CLUSTER;
                    $broken_syllables = \true;
                }
            }
            for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
                $o[$i]['syllable'] = $syllable_serial << 4 | $syllable_type;
            }
            $ptr += $syllable_length;
            $syllable_serial++;
            if ($syllable_serial == 16) {
                $syllable_serial = 1;
            }
        }
    }
    public static function set_syllables_sinhala(&$o, $s, &$broken_syllables)
    {
        $ptr = 0;
        $syllable_serial = 1;
        $broken_syllables = \false;
        while ($ptr < \strlen($s)) {
            $match = '';
            $syllable_length = 1;
            $syllable_type = self::NON_INDIC_CLUSTER;
            // CONSONANT_SYLLABLE Consonant syllable
            // From OT spec:
            if (\preg_match('/^([CR]HJ|[CR]JH){0,8}[CR][HM]{0,3}[S]{0,1}/', \substr($s, $ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::CONSONANT_SYLLABLE;
            } elseif (\preg_match('/^V[S]{0,1}/', \substr($s, $ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::VOWEL_SYLLABLE;
            }
            for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
                $o[$i]['syllable'] = $syllable_serial << 4 | $syllable_type;
            }
            $ptr += $syllable_length;
            $syllable_serial++;
            if ($syllable_serial == 16) {
                $syllable_serial = 1;
            }
        }
    }
    public static function set_syllables_khmer(&$o, $s, &$broken_syllables)
    {
        $ptr = 0;
        $syllable_serial = 1;
        $broken_syllables = \false;
        while ($ptr < \strlen($s)) {
            $match = '';
            $syllable_length = 1;
            $syllable_type = self::NON_INDIC_CLUSTER;
            // CONSONANT_SYLLABLE Consonant syllable
            if (\preg_match('/^r?([CR]J?((Z?F)?[N]{0,2})?[ZJ]?G(JN?)?){0,4}[CR]J?((Z?F)?[N]{0,2})?A?((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})?(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::CONSONANT_SYLLABLE;
            } elseif (\preg_match('/^(RH|r)?V((Z?F)?[N]{0,2})?(J|([ZJ]?G(JN?)?[CR]J?((Z?F)?[N]{0,2})?){0,4}((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})?(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2})/', \substr($s, $ptr), $ma)) {
                $syllable_length = \strlen($ma[0]);
                $syllable_type = self::VOWEL_SYLLABLE;
            } elseif (\preg_match('/^(RH|r)?((Z?F)?[N]{0,2})?(([ZJ]?G(JN?)?)[CR]J?((Z?F)?[N]{0,2})?){0,4}((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2}/', \substr($s, $ptr), $ma)) {
                if (\strlen($ma[0])) {
                    // May match blank
                    $syllable_length = \strlen($ma[0]);
                    $syllable_type = self::BROKEN_CLUSTER;
                    $broken_syllables = \true;
                }
            }
            for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
                $o[$i]['syllable'] = $syllable_serial << 4 | $syllable_type;
            }
            $ptr += $syllable_length;
            $syllable_serial++;
            if ($syllable_serial == 16) {
                $syllable_serial = 1;
            }
        }
    }
    public static function initial_reordering(&$info, $GSUBdata, $broken_syllables, $indic_config, $scriptblock, $is_old_spec, $dottedcircle)
    {
        self::update_consonant_positions($info, $GSUBdata);
        if ($broken_syllables && $dottedcircle) {
            self::insert_dotted_circles($info, $dottedcircle);
        }
        $count = \count($info);
        if (!$count) {
            return;
        }
        $last = 0;
        $last_syllable = $info[0]['syllable'];
        for ($i = 1; $i < $count; $i++) {
            if ($last_syllable != $info[$i]['syllable']) {
                self::initial_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $i);
                $last = $i;
                $last_syllable = $info[$last]['syllable'];
            }
        }
        self::initial_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $count);
    }
    public static function update_consonant_positions(&$info, $GSUBdata)
    {
        $count = \count($info);
        for ($i = 0; $i < $count; $i++) {
            if ($info[$i]['indic_position'] == self::POS_BASE_C) {
                $c = $info[$i]['uni'];
                // If would substitute...
                if (isset($GSUBdata['pref'][$c])) {
                    $info[$i]['indic_position'] = self::POS_POST_C;
                } elseif (isset($GSUBdata['blwf'][$c])) {
                    $info[$i]['indic_position'] = self::POS_BELOW_C;
                } elseif (isset($GSUBdata['pstf'][$c])) {
                    $info[$i]['indic_position'] = self::POS_POST_C;
                }
            }
        }
    }
    public static function insert_dotted_circles(&$info, $dottedcircle)
    {
        $idx = 0;
        $last_syllable = 0;
        while ($idx < \count($info)) {
            $syllable = $info[$idx]['syllable'];
            $syllable_type = $syllable & 0xf;
            if ($last_syllable != $syllable && $syllable_type == self::BROKEN_CLUSTER) {
                $last_syllable = $syllable;
                $dottedcircle[0]['syllable'] = $info[$idx]['syllable'];
                /* Insert dottedcircle after possible Repha. */
                while ($idx < \count($info) && $last_syllable == $info[$idx]['syllable'] && $info[$idx]['indic_category'] == self::OT_REPHA) {
                    $idx++;
                }
                \array_splice($info, $idx, 0, $dottedcircle);
            } else {
                $idx++;
            }
        }
        // I am not sue how this code below got in here, since $idx should now be > count($info) and thus invalid.
        // In case I am missing something(!) I'll leave a warning here for now:
        if (isset($info[$idx])) {
            throw new \DynamicOOOS\Mpdf\MpdfException('Unexpected error occurred in Indic processing');
        }
        // In case of final bloken cluster...
        //$syllable = $info[$idx]['syllable'];
        //$syllable_type = ($syllable & 0x0F);
        //if ($last_syllable != $syllable && $syllable_type == self::BROKEN_CLUSTER) {
        //	$dottedcircle[0]['syllable'] = $info[$idx]['syllable'];
        //	array_splice($info, $idx, 0, $dottedcircle);
        //}
    }
    /* Rules from:
     * https://www.microsoft.com/typography/otfntdev/devanot/shaping.aspx */
    public static function initial_reordering_syllable(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $start, $end)
    {
        /* vowel_syllable: We made the vowels look like consonants. So uses the consonant logic! */
        /* broken_cluster: We already inserted dotted-circles, so just call the standalone_cluster. */
        /* standalone_cluster: We treat NBSP/dotted-circle as if they are consonants, so we should just chain. */
        $syllable_type = $info[$start]['syllable'] & 0xf;
        if ($syllable_type == self::NON_INDIC_CLUSTER) {
            return;
        }
        if ($syllable_type == self::BROKEN_CLUSTER || $syllable_type == self::STANDALONE_CLUSTER) {
            //if ($uniscribe_bug_compatible) {
            /* For dotted-circle, this is what Uniscribe does:
             * If dotted-circle is the last glyph, it just does nothing.
             * i.e. It doesn't form Reph. */
            if ($info[$end - 1]['indic_category'] == self::OT_DOTTEDCIRCLE) {
                return;
            }
        }
        /* 1. Find base consonant:
         *
         * The shaping engine finds the base consonant of the syllable, using the
         * following algorithm: starting from the end of the syllable, move backwards
         * until a consonant is found that does not have a below-base or post-base
         * form (post-base forms have to follow below-base forms), or that is not a
         * pre-base reordering Ra, or arrive at the first consonant. The consonant
         * stopped at will be the base.
         *
         * 	o If the syllable starts with Ra + Halant (in a script that has Reph)
         * 	and has more than one consonant, Ra is excluded from candidates for
         * 	base consonants.
         */
        $base = $end;
        $has_reph = \false;
        $limit = $start;
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            /* -> If the syllable starts with Ra + Halant (in a script that has Reph)
             * 	and has more than one consonant, Ra is excluded from candidates for
             * 	base consonants. */
            if (\count($GSUBdata['rphf']) && $start + 3 <= $end && ($indic_config[4] == self::REPH_MODE_IMPLICIT && !self::is_joiner($info[$start + 2]) || $indic_config[4] == self::REPH_MODE_EXPLICIT && $info[$start + 2]['indic_category'] == self::OT_ZWJ)) {
                /* See if it matches the 'rphf' feature. */
                //$glyphs = array($info[$start]['uni'], $info[$start + 1]['uni']);
                //if ($indic_plan->rphf->would_substitute ($glyphs, count($glyphs), true, face)) {
                if (isset($GSUBdata['rphf'][$info[$start]['uni']]) && self::is_halant_or_coeng($info[$start + 1])) {
                    $limit += 2;
                    while ($limit < $end && self::is_joiner($info[$limit])) {
                        $limit++;
                    }
                    $base = $start;
                    $has_reph = \true;
                }
            } elseif ($indic_config[4] == self::REPH_MODE_LOG_REPHA && $info[$start]['indic_category'] == self::OT_REPHA) {
                $limit += 1;
                while ($limit < $end && self::is_joiner($info[$limit])) {
                    $limit++;
                }
                $base = $start;
                $has_reph = \true;
            }
        }
        switch ($indic_config[2]) {
            // base_pos
            case self::BASE_POS_LAST:
                /* -> starting from the end of the syllable, move backwards */
                $i = $end;
                $seen_below = \false;
                do {
                    $i--;
                    /* -> until a consonant is found */
                    if (self::is_consonant($info[$i])) {
                        /* -> that does not have a below-base or post-base form
                         * (post-base forms have to follow below-base forms), */
                        if ($info[$i]['indic_position'] != self::POS_BELOW_C && ($info[$i]['indic_position'] != self::POS_POST_C || $seen_below)) {
                            $base = $i;
                            break;
                        }
                        if ($info[$i]['indic_position'] == self::POS_BELOW_C) {
                            $seen_below = \true;
                        }
                        /* -> or that is not a pre-base reordering Ra,
                         *
                         * IMPLEMENTATION NOTES:
                         *
                         * Our pre-base reordering Ra's are marked POS_POST_C, so will be skipped
                         * by the logic above already.
                         */
                        /* -> or arrive at the first consonant. The consonant stopped at will
                         * be the base. */
                        $base = $i;
                    } else {
                        /* A ZWJ after a Halant stops the base search, and requests an explicit
                         * half form.
                         * [A ZWJ before a Halant, requests a subjoined form instead, and hence
                         * search continues. This is particularly important for Bengali
                         * sequence Ra,H,Ya that should form Ya-Phalaa by subjoining Ya] */
                        if ($start < $i && $info[$i]['indic_category'] == self::OT_ZWJ && $info[$i - 1]['indic_category'] == self::OT_H) {
                            if (!\defined("OMIT_INDIC_FIX_1") || OMIT_INDIC_FIX_1 != 1) {
                                $base = $i;
                            }
                            // INDIC_FIX_1
                            break;
                        }
                        // ZKI8
                        if ($start < $i && $info[$i]['indic_category'] == self::OT_ZWNJ) {
                            break;
                        }
                    }
                } while ($i > $limit);
                break;
            case self::BASE_POS_FIRST:
                /* In scripts without half forms (eg. Khmer), the first consonant is always the base. */
                if (!$has_reph) {
                    $base = $limit;
                }
                /* Find the last base consonant that is not blocked by ZWJ.	If there is
                 * a ZWJ right before a base consonant, that would request a subjoined form. */
                for ($i = $limit; $i < $end; $i++) {
                    if (self::is_consonant($info[$i]) && $info[$i]['indic_position'] == self::POS_BASE_C) {
                        if ($limit < $i && $info[$i - 1]['indic_category'] == self::OT_ZWJ) {
                            break;
                        } else {
                            $base = $i;
                        }
                    }
                }
                /* Mark all subsequent consonants as below. */
                for ($i = $base + 1; $i < $end; $i++) {
                    if (self::is_consonant($info[$i]) && $info[$i]['indic_position'] == self::POS_BASE_C) {
                        $info[$i]['indic_position'] = self::POS_BELOW_C;
                    }
                }
                break;
        }
        /* -> If the syllable starts with Ra + Halant (in a script that has Reph)
         * 	and has more than one consonant, Ra is excluded from candidates for
         * 	base consonants.
         *
         * 	Only do this for unforced Reph. (ie. not for Ra,H,ZWJ. */
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            if ($has_reph && $base == $start && $limit - $base <= 2) {
                /* Have no other consonant, so Reph is not formed and Ra becomes base. */
                $has_reph = \false;
            }
        }
        /* 2. Decompose and reorder Matras:
         *
         * Each matra and any syllable modifier sign in the cluster are moved to the
         * appropriate position relative to the consonant(s) in the cluster. The
         * shaping engine decomposes two- or three-part matras into their constituent
         * parts before any repositioning. Matra characters are classified by which
         * consonant in a conjunct they have affinity for and are reordered to the
         * following positions:
         *
         * 		o Before first half form in the syllable
         * 		o After subjoined consonants
         * 		o After post-form consonant
         * 		o After main consonant (for above marks)
         *
         * IMPLEMENTATION NOTES:
         *
         * The normalize() routine has already decomposed matras for us, so we don't
         * need to worry about that.
         */
        /* 3.	Reorder marks to canonical order:
         *
         * Adjacent nukta and halant or nukta and vedic sign are always repositioned
         * if necessary, so that the nukta is first.
         *
         * IMPLEMENTATION NOTES:
         *
         * Use the combining Class from Unicode categories? to bubble_sort.
         */
        /* Reorder characters */
        for ($i = $start; $i < $base; $i++) {
            $info[$i]['indic_position'] = \min(self::POS_PRE_C, $info[$i]['indic_position']);
        }
        if ($base < $end) {
            $info[$base]['indic_position'] = self::POS_BASE_C;
        }
        /* Mark final consonants. A final consonant is one appearing after a matra,
         * ? only in Khmer. */
        for ($i = $base + 1; $i < $end; $i++) {
            if ($info[$i]['indic_category'] == self::OT_M) {
                for ($j = $i + 1; $j < $end; $j++) {
                    if (self::is_consonant($info[$j])) {
                        $info[$j]['indic_position'] = self::POS_FINAL_C;
                        break;
                    }
                }
                break;
            }
        }
        /* Handle beginning Ra */
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            if ($has_reph) {
                $info[$start]['indic_position'] = self::POS_RA_TO_BECOME_REPH;
            }
        }
        /* For old-style Indic script tags, move the first post-base Halant after
         * last consonant.	Only do this if there is *not* a Halant after last
         * consonant. Otherwise it becomes messy. */
        if ($is_old_spec) {
            for ($i = $base + 1; $i < $end; $i++) {
                if ($info[$i]['indic_category'] == self::OT_H) {
                    for ($j = $end - 1; $j > $i; $j--) {
                        if (self::is_consonant($info[$j]) || $info[$j]['indic_category'] == self::OT_H) {
                            break;
                        }
                    }
                    if ($info[$j]['indic_category'] != self::OT_H && $j > $i) {
                        /* Move Halant to after last consonant. */
                        self::_move_info_pos($info, $i, $j + 1);
                    }
                    break;
                }
            }
        }
        /* Attach misc marks to previous char to move with them. */
        $last_pos = self::POS_START;
        for ($i = $start; $i < $end; $i++) {
            if (self::FLAG($info[$i]['indic_category']) & (self::FLAG(self::OT_ZWJ) | self::FLAG(self::OT_ZWNJ) | self::FLAG(self::OT_N) | self::FLAG(self::OT_RS) | self::FLAG(self::OT_H) | self::FLAG(self::OT_COENG))) {
                $info[$i]['indic_position'] = $last_pos;
                if ($info[$i]['indic_category'] == self::OT_H && $info[$i]['indic_position'] == self::POS_PRE_M) {
                    /*
                     * Uniscribe doesn't move the Halant with Left Matra.
                     * TEST: U+092B,U+093F,U+094DE
                     * We follow.	This is important for the Sinhala
                     * U+0DDA split matra since it decomposes to U+0DD9,U+0DCA
                     * where U+0DD9 is a left matra and U+0DCA is the virama.
                     * We don't want to move the virama with the left matra.
                     * TEST: U+0D9A,U+0DDA
                     */
                    for ($j = $i; $j > $start; $j--) {
                        if ($info[$j - 1]['indic_position'] != self::POS_PRE_M) {
                            $info[$i]['indic_position'] = $info[$j - 1]['indic_position'];
                            break;
                        }
                    }
                }
            } elseif ($info[$i]['indic_position'] != self::POS_SMVD) {
                $last_pos = $info[$i]['indic_position'];
            }
        }
        /* Re-attach ZWJ, ZWNJ, and halant to next char, for after-base consonants. */
        $last_halant = $end;
        for ($i = $base + 1; $i < $end; $i++) {
            if (self::is_halant_or_coeng($info[$i])) {
                $last_halant = $i;
            } elseif (self::is_consonant($info[$i])) {
                for ($j = $last_halant; $j < $i; $j++) {
                    if ($info[$j]['indic_position'] != self::POS_SMVD) {
                        $info[$j]['indic_position'] = $info[$i]['indic_position'];
                    }
                }
            }
        }
        if ($scriptblock == Ucdn::SCRIPT_KHMER) {
            /* KHMER_FIX_2 */
            /* Move Coeng+RO (Halant,Ra) sequence before base consonant. */
            for ($i = $base + 1; $i < $end; $i++) {
                if (self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni'])) {
                    $info[$i]['indic_position'] = self::POS_PRE_C;
                    $info[$i + 1]['indic_position'] = self::POS_PRE_C;
                    break;
                }
            }
        }
        /*
         if (!defined("OMIT_INDIC_FIX_2") || OMIT_INDIC_FIX_2 != 1) {
         // INDIC_FIX_2
         $ZWNJ_found = false;
         $POST_ZWNJ_c_found = false;
         for ($i = $base + 1; $i < $end; $i++) {
         if ($info[$i]['indic_category'] == self::OT_ZWNJ) { $ZWNJ_found = true; }
         else if ($ZWNJ_found && $info[$i]['indic_category'] == self::OT_C) { $POST_ZWNJ_c_found = true; }
         else if ($POST_ZWNJ_c_found && $info[$i]['indic_position'] == self::POS_BEFORE_SUB) { $info[$i]['indic_position'] = self::POS_AFTER_SUB; }
         }
         }
        */
        /* Setup masks now */
        for ($i = $start; $i < $end; $i++) {
            $info[$i]['mask'] = 0;
        }
        if ($scriptblock == Ucdn::SCRIPT_KHMER) {
            /* Find a Coeng+RO (Halant,Ra) sequence and mark it for pre-base processing. */
            $mask = self::FLAG(self::PREF);
            for ($i = $base; $i < $end - 1; $i++) {
                /* KHMER_FIX_1 From $start (not base) */
                if (self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni'])) {
                    $info[$i]['mask'] |= self::FLAG(self::PREF);
                    $info[$i + 1]['mask'] |= self::FLAG(self::PREF);
                    /* Mark the subsequent stuff with 'cfar'.  Used in Khmer.
                     * Read the feature spec.
                     * This allows distinguishing the following cases with MS Khmer fonts:
                     * U+1784,U+17D2,U+179A,U+17D2,U+1782  [C+Coeng+RO+Coeng+C] => Should activate CFAR
                     * U+1784,U+17D2,U+1782,U+17D2,U+179A  [C+Coeng+C+Coeng+RO] => Should NOT activate CFAR
                     */
                    for ($j = $i + 2; $j < $end; $j++) {
                        $info[$j]['mask'] |= self::FLAG(self::CFAR);
                    }
                    break;
                }
            }
        }
        /* Sit tight, rock 'n roll! */
        self::bubble_sort($info, $start, $end - $start);
        /* Find base again */
        $base = $end;
        for ($i = $start; $i < $end; $i++) {
            if ($info[$i]['indic_position'] == self::POS_BASE_C) {
                $base = $i;
                break;
            }
        }
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            /* Reph */
            for ($i = $start; $i < $end; $i++) {
                if ($info[$i]['indic_position'] == self::POS_RA_TO_BECOME_REPH) {
                    $info[$i]['mask'] |= self::FLAG(self::RPHF);
                }
            }
            /* Pre-base */
            $mask = self::FLAG(self::HALF);
            for ($i = $start; $i < $base; $i++) {
                $info[$i]['mask'] |= $mask;
            }
        }
        /* Post-base */
        $mask = self::FLAG(self::BLWF) | self::FLAG(self::ABVF) | self::FLAG(self::PSTF);
        for ($i = $base + 1; $i < $end; $i++) {
            $info[$i]['mask'] |= $mask;
        }
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            if (!\defined("OMIT_INDIC_FIX_3") || OMIT_INDIC_FIX_3 != 1) {
                /* INDIC_FIX_3 */
                /* Find a (pre-base) Consonant, Halant,Ra sequence and mark Halant|Ra for below-base BLWF processing. */
                // TEST CASE &#x995;&#x9cd;&#x9b0;&#x9cd;&#x995; in FreeSans versus Vrinda
                if ($base - $start >= 3) {
                    for ($i = $start; $i < $base - 2; $i++) {
                        if (self::is_consonant($info[$i])) {
                            if (self::is_halant_or_coeng($info[$i + 1]) && self::is_ra($info[$i + 2]['uni'])) {
                                // If would substitute Halant+Ra...BLWF
                                if (isset($GSUBdata['blwf'][$info[$i + 2]['uni']])) {
                                    $info[$i + 1]['mask'] |= self::FLAG(self::BLWF);
                                    $info[$i + 2]['mask'] |= self::FLAG(self::BLWF);
                                } elseif (self::is_halant_or_coeng($info[$i + 3])) {
                                    $info[$i + 2]['mask'] |= self::FLAG(self::RPHF);
                                    $info[$i + 3]['mask'] |= self::FLAG(self::RPHF);
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
        if ($is_old_spec && $scriptblock == Ucdn::SCRIPT_DEVANAGARI) {
            /* Old-spec eye-lash Ra needs special handling.	From the spec:
             * "The feature 'below-base form' is applied to consonants
             * having below-base forms and following the base consonant.
             * The exception is vattu, which may appear below half forms
             * as well as below the base glyph. The feature 'below-base
             * form' will be applied to all such occurrences of Ra as well."
             *
             * Test case: U+0924,U+094D,U+0930,U+094d,U+0915
             * with Sanskrit 2003 font.
             *
             * However, note that Ra,Halant,ZWJ is the correct way to
             * request eyelash form of Ra, so we wouldbn't inhibit it
             * in that sequence.
             *
             * Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915
             */
            for ($i = $start; $i + 1 < $base; $i++) {
                if ($info[$i]['indic_category'] == self::OT_RA && $info[$i + 1]['indic_category'] == self::OT_H && ($i + 2 == $base || $info[$i + 2]['indic_category'] != self::OT_ZWJ)) {
                    $info[$i]['mask'] |= self::FLAG(self::BLWF);
                    $info[$i + 1]['mask'] |= self::FLAG(self::BLWF);
                }
            }
        }
        if ($scriptblock != Ucdn::SCRIPT_KHMER) {
            if (\count($GSUBdata['pref']) && $base + 2 < $end) {
                /* Find a Halant,Ra sequence and mark it for pre-base processing. */
                for ($i = $base + 1; $i + 1 < $end; $i++) {
                    // If old_spec find Ra-Halant...
                    if (isset($GSUBdata['pref'][$info[$i + 1]['uni']]) && self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni']) || $is_old_spec && isset($GSUBdata['pref'][$info[$i]['uni']]) && self::is_halant_or_coeng($info[$i + 1]) && self::is_ra($info[$i]['uni'])) {
                        $info[$i++]['mask'] |= self::FLAG(self::PREF);
                        $info[$i++]['mask'] |= self::FLAG(self::PREF);
                        break;
                    }
                }
            }
        }
        /* Apply ZWJ/ZWNJ effects */
        for ($i = $start + 1; $i < $end; $i++) {
            if (self::is_joiner($info[$i])) {
                $non_joiner = $info[$i]['indic_category'] == self::OT_ZWNJ;
                $j = $i;
                while ($j > $start) {
                    if (\defined("OMIT_INDIC_FIX_4") && OMIT_INDIC_FIX_4 == 1) {
                        // INDIC_FIX_4 = do nothing - carry on //
                        // ZWNJ should block H C from forming blwf post-base - need to unmask backwards beyond first consonant arrived at //
                        if (!self::is_consonant($info[$j])) {
                            break;
                        }
                    }
                    $j--;
                    /* ZWJ/ZWNJ should disable CJCT.	They do that by simply
                     * being there, since we don't skip them for the CJCT
                     * feature (ie. F_MANUAL_ZWJ) */
                    /* A ZWNJ disables HALF. */
                    if ($non_joiner) {
                        $info[$j]['mask'] &= ~(self::FLAG(self::HALF) | self::FLAG(self::BLWF));
                    }
                }
            }
        }
    }
    public static function final_reordering(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec)
    {
        $count = \count($info);
        if (!$count) {
            return;
        }
        $last = 0;
        $last_syllable = $info[0]['syllable'];
        for ($i = 1; $i < $count; $i++) {
            if ($last_syllable != $info[$i]['syllable']) {
                self::final_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $i);
                $last = $i;
                $last_syllable = $info[$last]['syllable'];
            }
        }
        self::final_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $count);
    }
    public static function final_reordering_syllable(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $start, $end)
    {
        /* 4. Final reordering:
         *
         * After the localized forms and basic shaping forms GSUB features have been
         * applied (see below), the shaping engine performs some final glyph
         * reordering before applying all the remaining font features to the entire
         * cluster.
         */
        /* Find base again */
        for ($base = $start; $base < $end; $base++) {
            if ($info[$base]['indic_position'] >= self::POS_BASE_C) {
                if ($start < $base && $info[$base]['indic_position'] > self::POS_BASE_C) {
                    $base--;
                }
                break;
            }
        }
        if ($base == $end && $start < $base && $info[$base - 1]['indic_category'] != self::OT_ZWJ) {
            $base--;
        }
        while ($start < $base && isset($info[$base]) && ($info[$base]['indic_category'] == self::OT_H || $info[$base]['indic_category'] == self::OT_N)) {
            $base--;
        }
        /* 	o Reorder matras:
         *
         * 	If a pre-base matra character had been reordered before applying basic
         * 	features, the glyph can be moved closer to the main consonant based on
         * 	whether half-forms had been formed. Actual position for the matra is
         * 	defined as "after last standalone halant glyph, after initial matra
         * 	position and before the main consonant". If ZWJ or ZWNJ follow this
         * 	halant, position is moved after it.
         */
        if ($start + 1 < $end && $start < $base) {
            /* Otherwise there can't be any pre-base matra characters. */
            /* If we lost track of base, alas, position before last thingy. */
            $new_pos = $base == $end ? $base - 2 : $base - 1;
            /* Malayalam / Tamil do not have "half" forms or explicit virama forms.
             * The glyphs formed by 'half' are Chillus or ligated explicit viramas.
             * We want to position matra after them.
             */
            if ($scriptblock != Ucdn::SCRIPT_MALAYALAM && $scriptblock != Ucdn::SCRIPT_TAMIL) {
                while ($new_pos > $start && !self::is_one_of($info[$new_pos], self::FLAG(self::OT_M) | self::FLAG(self::OT_H) | self::FLAG(self::OT_COENG))) {
                    $new_pos--;
                }
                /* If we found no Halant we are done.
                 * Otherwise only proceed if the Halant does
                 * not belong to the Matra itself! */
                if (self::is_halant_or_coeng($info[$new_pos]) && $info[$new_pos]['indic_position'] != self::POS_PRE_M) {
                    /* -> If ZWJ or ZWNJ follow this halant, position is moved after it. */
                    if ($new_pos + 1 < $end && self::is_joiner($info[$new_pos + 1])) {
                        $new_pos++;
                    }
                } else {
                    $new_pos = $start;
                }
                /* No move. */
            }
            if ($start < $new_pos && $info[$new_pos]['indic_position'] != self::POS_PRE_M) {
                /* Now go see if there's actually any matras... */
                for ($i = $new_pos; $i > $start; $i--) {
                    if ($info[$i - 1]['indic_position'] == self::POS_PRE_M) {
                        $old_pos = $i - 1;
                        //memmove (&info[$old_pos], &info[$old_pos + 1], ($new_pos - $old_pos) * sizeof ($info[0]));
                        self::_move_info_pos($info, $old_pos, $new_pos + 1);
                        if ($old_pos < $base && $base <= $new_pos) {
                            /* Shouldn't actually happen. */
                            $base--;
                        }
                        $new_pos--;
                    }
                }
            }
        }
        /* 	o Reorder reph:
         *
         * 	Reph's original position is always at the beginning of the syllable,
         * 	(i.e. it is not reordered at the character reordering stage). However,
         * 	it will be reordered according to the basic-forms shaping results.
         * 	Possible positions for reph, depending on the script, are; after main,
         * 	before post-base consonant forms, and after post-base consonant forms.
         */
        /* If there's anything after the Ra that has the REPH pos, it ought to be halant.
         * Which means that the font has failed to ligate the Reph.	In which case, we
         * shouldn't move. */
        if ($start + 1 < $end && $info[$start]['indic_position'] == self::POS_RA_TO_BECOME_REPH && $info[$start + 1]['indic_position'] != self::POS_RA_TO_BECOME_REPH) {
            $reph_pos = $indic_config[3];
            $skip_to_reph_step_5 = \false;
            $skip_to_reph_move = \false;
            /* 	1. If reph should be positioned after post-base consonant forms,
             * 	proceed to step 5.
             */
            if ($reph_pos == self::REPH_POS_AFTER_POST) {
                $skip_to_reph_step_5 = \true;
            }
            /* 	2. If the reph repositioning class is not after post-base: target
             * 	position is after the first explicit halant glyph between the
             * 	first post-reph consonant and last main consonant. If ZWJ or ZWNJ
             * 	are following this halant, position is moved after it. If such
             * 	position is found, this is the target position. Otherwise,
             * 	proceed to the next step.
             *
             * 	Note: in old-implementation fonts, where classifications were
             * 	fixed in shaping engine, there was no case where reph position
             * 	will be found on this step.
             */
            if (!$skip_to_reph_step_5) {
                $new_reph_pos = $start + 1;
                while ($new_reph_pos < $base && !self::is_halant_or_coeng($info[$new_reph_pos])) {
                    $new_reph_pos++;
                }
                if ($new_reph_pos < $base && self::is_halant_or_coeng($info[$new_reph_pos])) {
                    /* ->If ZWJ or ZWNJ are following this halant, position is moved after it. */
                    if ($new_reph_pos + 1 < $base && self::is_joiner($info[$new_reph_pos + 1])) {
                        $new_reph_pos++;
                    }
                    $skip_to_reph_move = \true;
                }
            }
            /* 	3. If reph should be repositioned after the main consonant: find the
             * 	first consonant not ligated with main, or find the first
             * 	consonant that is not a potential pre-base reordering Ra.
             */
            if ($reph_pos == self::REPH_POS_AFTER_MAIN && !$skip_to_reph_move && !$skip_to_reph_step_5) {
                $new_reph_pos = $base;
                /* XXX Skip potential pre-base reordering Ra. */
                while ($new_reph_pos + 1 < $end && $info[$new_reph_pos + 1]['indic_position'] <= self::POS_AFTER_MAIN) {
                    $new_reph_pos++;
                }
                if ($new_reph_pos < $end) {
                    $skip_to_reph_move = \true;
                }
            }
            /* 	4. If reph should be positioned before post-base consonant, find
             * 	first post-base classified consonant not ligated with main. If no
             * 	consonant is found, the target position should be before the
             * 	first matra, syllable modifier sign or vedic sign.
             */
            /* This is our take on what step 4 is trying to say (and failing, BADLY). */
            if ($reph_pos == self::REPH_POS_AFTER_SUB && !$skip_to_reph_move && !$skip_to_reph_step_5) {
                $new_reph_pos = $base;
                while ($new_reph_pos < $end && isset($info[$new_reph_pos + 1]['indic_position']) && !(self::FLAG($info[$new_reph_pos + 1]['indic_position']) & (self::FLAG(self::POS_POST_C) | self::FLAG(self::POS_AFTER_POST) | self::FLAG(self::POS_SMVD)))) {
                    $new_reph_pos++;
                }
                if ($new_reph_pos < $end) {
                    $skip_to_reph_move = \true;
                }
            }
            /* 	5. If no consonant is found in steps 3 or 4, move reph to a position
             * 		immediately before the first post-base matra, syllable modifier
             * 		sign or vedic sign that has a reordering class after the intended
             * 		reph position. For example, if the reordering position for reph
             * 		is post-main, it will skip above-base matras that also have a
             * 		post-main position.
             */
            if (!$skip_to_reph_move) {
                /* Copied from step 2. */
                $new_reph_pos = $start + 1;
                while ($new_reph_pos < $base && !self::is_halant_or_coeng($info[$new_reph_pos])) {
                    $new_reph_pos++;
                }
                if ($new_reph_pos < $base && self::is_halant_or_coeng($info[$new_reph_pos])) {
                    /* ->If ZWJ or ZWNJ are following this halant, position is moved after it. */
                    if ($new_reph_pos + 1 < $base && self::is_joiner($info[$new_reph_pos + 1])) {
                        $new_reph_pos++;
                    }
                    $skip_to_reph_move = \true;
                }
            }
            /* 	6. Otherwise, reorder reph to the end of the syllable.
             */
            if (!$skip_to_reph_move) {
                $new_reph_pos = $end - 1;
                while ($new_reph_pos > $start && $info[$new_reph_pos]['indic_position'] == self::POS_SMVD) {
                    $new_reph_pos--;
                }
                /*
                 * If the Reph is to be ending up after a Matra,Halant sequence,
                 * position it before that Halant so it can interact with the Matra.
                 * However, if it's a plain Consonant,Halant we shouldn't do that.
                 * Uniscribe doesn't do this.
                 * TEST: U+0930,U+094D,U+0915,U+094B,U+094D
                 */
                //if (!$hb_options.uniscribe_bug_compatible && self::is_halant_or_coeng($info[$new_reph_pos])) {
                if (self::is_halant_or_coeng($info[$new_reph_pos])) {
                    for ($i = $base + 1; $i < $new_reph_pos; $i++) {
                        if ($info[$i]['indic_category'] == self::OT_M) {
                            /* Ok, got it. */
                            $new_reph_pos--;
                        }
                    }
                }
            }
            /* Move */
            self::_move_info_pos($info, $start, $new_reph_pos + 1);
            if ($start < $base && $base <= $new_reph_pos) {
                $base--;
            }
        }
        /* 	o Reorder pre-base reordering consonants:
         *
         * 	If a pre-base reordering consonant is found, reorder it according to
         * 	the following rules:
         */
        if (\count($GSUBdata['pref']) && $base + 1 < $end) {
            /* Otherwise there can't be any pre-base reordering Ra. */
            for ($i = $base + 1; $i < $end; $i++) {
                if ($info[$i]['mask'] & self::FLAG(self::PREF)) {
                    /* 	1. Only reorder a glyph produced by substitution during application
                     * 	of the <pref> feature. (Note that a font may shape a Ra consonant with
                     * 	the feature generally but block it in certain contexts.)
                     */
                    // ??? Need to TEST if actual substitution has occurred
                    if ($i + 1 == $end || ($info[$i + 1]['mask'] & self::FLAG(self::PREF)) == 0) {
                        /*
                         * 	2. Try to find a target position the same way as for pre-base matra.
                         * 	If it is found, reorder pre-base consonant glyph.
                         *
                         * 	3. If position is not found, reorder immediately before main
                         * 	consonant.
                         */
                        $new_pos = $base;
                        /* Malayalam / Tamil do not have "half" forms or explicit virama forms.
                         * The glyphs formed by 'half' are Chillus or ligated explicit viramas.
                         * We want to position matra after them.
                         */
                        if ($scriptblock != Ucdn::SCRIPT_MALAYALAM && $scriptblock != Ucdn::SCRIPT_TAMIL) {
                            while ($new_pos > $start && !self::is_one_of($info[$new_pos - 1], self::FLAG(self::OT_M) | self::FLAG(self::OT_H) | self::FLAG(self::OT_COENG))) {
                                $new_pos--;
                            }
                            /* In Khmer coeng model, a V,Ra can go *after* matras. If it goes after a
                             * split matra, it should be reordered to *before* the left part of such matra. */
                            if ($new_pos > $start && $info[$new_pos - 1]['indic_category'] == self::OT_M) {
                                $old_pos = $i;
                                for ($i = $base + 1; $i < $old_pos; $i++) {
                                    if ($info[$i]['indic_category'] == self::OT_M) {
                                        $new_pos--;
                                        break;
                                    }
                                }
                            }
                        }
                        if ($new_pos > $start && self::is_halant_or_coeng($info[$new_pos - 1])) {
                            /* -> If ZWJ or ZWNJ follow this halant, position is moved after it. */
                            if ($new_pos < $end && self::is_joiner($info[$new_pos])) {
                                $new_pos++;
                            }
                        }
                        $old_pos = $i;
                        self::_move_info_pos($info, $old_pos, $new_pos);
                        if ($new_pos <= $base && $base < $old_pos) {
                            $base++;
                        }
                    }
                    break;
                }
            }
        }
        /* Apply 'init' to the Left Matra if it's a word start. */
        if ($info[$start]['indic_position'] == self::POS_PRE_M && ($start == 0 || ($info[$start - 1]['general_category'] < Ucdn::UNICODE_GENERAL_CATEGORY_FORMAT || $info[$start - 1]['general_category'] > Ucdn::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK))) {
            $info[$start]['mask'] |= self::FLAG(self::INIT);
        }
        /*
         * Finish off and go home!
         */
    }
    public static function _move_info_pos(&$info, $from, $to)
    {
        $t = [];
        $t[0] = $info[$from];
        if ($from > $to) {
            \array_splice($info, $from, 1);
            \array_splice($info, $to, 0, $t);
        } else {
            \array_splice($info, $to, 0, $t);
            \array_splice($info, $from, 1);
        }
    }
    public static $ra_chars = [
        0x930 => 1,
        /* Devanagari */
        0x9b0 => 1,
        /* Bengali */
        0x9f0 => 1,
        /* Bengali (Assamese) */
        0xa30 => 1,
        /* Gurmukhi */
        /* No Reph */
        0xab0 => 1,
        /* Gujarati */
        0xb30 => 1,
        /* Oriya */
        0xbb0 => 1,
        /* Tamil */
        /* No Reph */
        0xc30 => 1,
        /* Telugu */
        /* Reph formed only with ZWJ */
        0xcb0 => 1,
        /* Kannada */
        0xd30 => 1,
        /* Malayalam */
        /* No Reph, Logical Repha */
        0xdbb => 1,
        /* Sinhala */
        /* Reph formed only with ZWJ */
        0x179a => 1,
    ];
    public static function is_ra($u)
    {
        return isset(self::$ra_chars[$u]);
    }
    public static function is_one_of($info, $flags)
    {
        if (isset($info['is_ligature']) && $info['is_ligature']) {
            return \false;
        }
        /* If it ligated, all bets are off. */
        return !!(self::FLAG($info['indic_category']) & $flags);
    }
    public static function is_joiner($info)
    {
        return self::is_one_of($info, self::FLAG(self::OT_ZWJ) | self::FLAG(self::OT_ZWNJ));
    }
    /* Vowels and placeholders treated as if they were consonants. */
    public static function is_consonant($info)
    {
        return self::is_one_of($info, self::FLAG(self::OT_C) | self::FLAG(self::OT_CM) | self::FLAG(self::OT_RA) | self::FLAG(self::OT_V) | self::FLAG(self::OT_NBSP) | self::FLAG(self::OT_DOTTEDCIRCLE));
    }
    public static function is_halant_or_coeng($info)
    {
        return self::is_one_of($info, self::FLAG(self::OT_H) | self::FLAG(self::OT_COENG));
    }
    // From hb-private.hh
    public static function in_range($u, $lo, $hi)
    {
        if ((($lo ^ $hi) & $lo) == 0 && (($lo ^ $hi) & $hi) == ($lo ^ $hi) && (($lo ^ $hi) & ($lo ^ $hi) + 1) == 0) {
            return ($u & ~($lo ^ $hi)) == $lo;
        } else {
            return $lo <= $u && $u <= $hi;
        }
    }
    // From hb-private.hh
    public static function FLAG($x)
    {
        return 1 << $x;
    }
    // BELOW from hb-ot-shape-complex-indic.cc
    /*
     * Indic configurations.
     */
    // base_position
    const BASE_POS_FIRST = 0;
    const BASE_POS_LAST = 1;
    // reph_position
    const REPH_POS_DEFAULT = 10;
    // POS_BEFORE_POST,
    const REPH_POS_AFTER_MAIN = 5;
    // POS_AFTER_MAIN,
    const REPH_POS_BEFORE_SUB = 7;
    // POS_BEFORE_SUB,
    const REPH_POS_AFTER_SUB = 9;
    // POS_AFTER_SUB,
    const REPH_POS_BEFORE_POST = 10;
    // POS_BEFORE_POST,
    const REPH_POS_AFTER_POST = 12;
    // POS_AFTER_POST
    // reph_mode
    const REPH_MODE_IMPLICIT = 0;
    /* Reph formed out of initial Ra,H sequence. */
    const REPH_MODE_EXPLICIT = 1;
    /* Reph formed out of initial Ra,H,ZWJ sequence. */
    const REPH_MODE_VIS_REPHA = 2;
    /* Encoded Repha character, no reordering needed. */
    const REPH_MODE_LOG_REPHA = 3;
    /* Encoded Repha character, needs reordering. */
    /*
     struct of indic_configs{
     KEY - script;
     0 - has_old_spec;
     1 - virama;
     2 - base_pos;
     3 - reph_pos;
     4 - reph_mode;
     };
    */
    public static $indic_configs = [
        /* index is SCRIPT_number from UCDN */
        9 => [\true, 0x94d, 1, 10, 0],
        10 => [\true, 0x9cd, 1, 9, 0],
        11 => [\true, 0xa4d, 1, 7, 0],
        12 => [\true, 0xacd, 1, 10, 0],
        13 => [\true, 0xb4d, 1, 5, 0],
        14 => [\true, 0xbcd, 1, 12, 0],
        15 => [\true, 0xc4d, 1, 12, 1],
        16 => [\true, 0xccd, 1, 12, 0],
        17 => [\true, 0xd4d, 1, 5, 3],
        18 => [\false, 0xdca, 0, 5, 1],
        /* Sinhala */
        30 => [\false, 0x17d2, 0, 10, 2],
        /* Khmer */
        84 => [\false, 0xa9c0, 1, 10, 0],
    ];
    /*
     // from "hb-ot-shape-complex-indic-table.cc"
    
     const ISC_A	 = 0; //	INDIC_SYLLABIC_CATEGORY_AVAGRAHA		Avagraha
    	  const ISC_Bi = 8; //	INDIC_SYLLABIC_CATEGORY_BINDU			Bindu
    	  const ISC_C	 = 1; //	INDIC_SYLLABIC_CATEGORY_CONSONANT		Consonant
    	  const ISC_CD = 1; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_DEAD		Consonant_Dead
    	  const ISC_CF = 17; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_FINAL		Consonant_Final
    	  const ISC_CHL = 1; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_HEAD_LETTER	Consonant_Head_Letter
    	  const ISC_CM = 17; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_MEDIAL		Consonant_Medial
    	  const ISC_CP = 11; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_PLACEHOLDER	Consonant_Placeholder
    	  const ISC_CR = 15; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_REPHA		Consonant_Repha
    	  const ISC_CS = 1; //	INDIC_SYLLABIC_CATEGORY_CONSONANT_SUBJOINED	Consonant_Subjoined
    	  const ISC_ML = 0; //	INDIC_SYLLABIC_CATEGORY_MODIFYING_LETTER	Modifying_Letter
    	  const ISC_N	 = 3; //	INDIC_SYLLABIC_CATEGORY_NUKTA			Nukta
    	  const ISC_x	 = 0; //	INDIC_SYLLABIC_CATEGORY_OTHER			Other
    	  const ISC_RS = 13; //	INDIC_SYLLABIC_CATEGORY_REGISTER_SHIFTER	Register_Shifter
    	  const ISC_TL = 0; //	INDIC_SYLLABIC_CATEGORY_TONE_LETTER		Tone_Letter
    	  const ISC_TM = 3; //	INDIC_SYLLABIC_CATEGORY_TONE_MARK		Tone_Mark
    	  const ISC_V	 = 4; //	INDIC_SYLLABIC_CATEGORY_VIRAMA		Virama
    	  const ISC_Vs = 8; //	INDIC_SYLLABIC_CATEGORY_VISARGA		Visarga
    	  const ISC_Vo = 2; //	INDIC_SYLLABIC_CATEGORY_VOWEL			Vowel
    	  const ISC_M	 = 7; //	INDIC_SYLLABIC_CATEGORY_VOWEL_DEPENDENT	Vowel_Dependent
    	  const ISC_VI = 2; //	INDIC_SYLLABIC_CATEGORY_VOWEL_INDEPENDENT	Vowel_Independent
     const IMC_B	 = 8; //	INDIC_MATRA_CATEGORY_BOTTOM			Bottom
    	  const IMC_BR = 11; //	INDIC_MATRA_CATEGORY_BOTTOM_AND_RIGHT	Bottom_And_Right
    	  const IMC_I	 = 15; //	INDIC_MATRA_CATEGORY_INVISIBLE		Invisible
    	  const IMC_L	 = 3; //	INDIC_MATRA_CATEGORY_LEFT			Left
    	  const IMC_LR = 11; //	INDIC_MATRA_CATEGORY_LEFT_AND_RIGHT		Left_And_Right
    	  const IMC_x	 = 15; //	INDIC_MATRA_CATEGORY_NOT_APPLICABLE		Not_Applicable
    	  const IMC_O	 = 5; //	INDIC_MATRA_CATEGORY_OVERSTRUCK		Overstruck
    	  const IMC_R	 = 11; //	INDIC_MATRA_CATEGORY_RIGHT			Right
    	  const IMC_T	 = 6; //	INDIC_MATRA_CATEGORY_TOP			Top
    	  const IMC_TB = 8; //	INDIC_MATRA_CATEGORY_TOP_AND_BOTTOM		Top_And_Bottom
    	  const IMC_TBR = 11; //	INDIC_MATRA_CATEGORY_TOP_AND_BOTTOM_AND_RIGHT	Top_And_Bottom_And_Right
    	  const IMC_TL = 6; //	INDIC_MATRA_CATEGORY_TOP_AND_LEFT		Top_And_Left
    	  const IMC_TLR = 11; //	INDIC_MATRA_CATEGORY_TOP_AND_LEFT_AND_RIGHT	Top_And_Left_And_Right
    	  const IMC_TR = 11; //	INDIC_MATRA_CATEGORY_TOP_AND_RIGHT		Top_And_Right
    	  const IMC_VOL = 2; //	INDIC_MATRA_CATEGORY_VISUAL_ORDER_LEFT		Visual_Order_Left
     If in original table = _(C,x), that = ISC_C,IMC_x
    	  Value is IMC_x << 8 (or IMC_x * 256) = 3840
    	  plus ISC_C = 1, so = 3841
    */
    public static $indic_table = [
        /* Devanagari  (0900..097F) */
        /* 0900 */
        3848,
        3848,
        3848,
        3848,
        3842,
        3842,
        3842,
        3842,
        /* 0908 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        /* 0910 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0918 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0920 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0928 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0930 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0938 */
        3841,
        3841,
        1543,
        2823,
        3843,
        3840,
        2823,
        775,
        /* 0940 */
        2823,
        2055,
        2055,
        2055,
        2055,
        1543,
        1543,
        1543,
        /* 0948 */
        1543,
        2823,
        2823,
        2823,
        2823,
        2052,
        775,
        2823,
        /* 0950 */
        3840,
        3840,
        3840,
        3840,
        3840,
        1543,
        2055,
        2055,
        /* 0958 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0960 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0968 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0970 */
        3840,
        3840,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        /* 0978 */
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* Bengali  (0980..09FF) */
        /* 0980 */
        3840,
        3848,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0988 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3840,
        3842,
        /* 0990 */
        3842,
        3840,
        3840,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0998 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 09A0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 09A8 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 09B0 */
        3841,
        3840,
        3841,
        3840,
        3840,
        3840,
        3841,
        3841,
        /* 09B8 */
        3841,
        3841,
        3840,
        3840,
        3843,
        3840,
        2823,
        775,
        /* 09C0 */
        2823,
        2055,
        2055,
        2055,
        2055,
        3840,
        3840,
        775,
        /* 09C8 */
        775,
        3840,
        3840,
        2823,
        2823,
        2052,
        3841,
        3840,
        /* 09D0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        2823,
        /* 09D8 */
        3840,
        3840,
        3840,
        3840,
        3841,
        3841,
        3840,
        3841,
        /* 09E0 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 09E8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 09F0 */
        3841,
        3841,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 09F8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Gurmukhi  (0A00..0A7F) */
        /* 0A00 */
        3840,
        3848,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0A08 */
        3842,
        3842,
        3842,
        3840,
        3840,
        3840,
        3840,
        3842,
        /* 0A10 */
        3842,
        3840,
        3840,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0A18 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0A20 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0A28 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0A30 */
        3841,
        3840,
        3841,
        3841,
        3840,
        3841,
        3841,
        3840,
        /* 0A38 */
        3841,
        3841,
        3840,
        3840,
        3843,
        3840,
        2823,
        775,
        /* 0A40 */
        2823,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        1543,
        /* 0A48 */
        1543,
        3840,
        3840,
        1543,
        1543,
        2052,
        3840,
        3840,
        /* 0A50 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0A58 */
        3840,
        3841,
        3841,
        3841,
        3841,
        3840,
        3841,
        3840,
        /* 0A60 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0A68 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0A70 */
        3848,
        3840,
        13841,
        13841,
        3840,
        3857,
        3840,
        3840,
        /* 0A78 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Gujarati  (0A80..0AFF) */
        /* 0A80 */
        3840,
        3848,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0A88 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3842,
        /* 0A90 */
        3842,
        3842,
        3840,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0A98 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0AA0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0AA8 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0AB0 */
        3841,
        3840,
        3841,
        3841,
        3840,
        3841,
        3841,
        3841,
        /* 0AB8 */
        3841,
        3841,
        3840,
        3840,
        3843,
        3840,
        2823,
        775,
        /* 0AC0 */
        2823,
        2055,
        2055,
        2055,
        2055,
        1543,
        3840,
        1543,
        /* 0AC8 */
        1543,
        2823,
        3840,
        2823,
        2823,
        2052,
        3840,
        3840,
        /* 0AD0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0AD8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0AE0 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0AE8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0AF0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0AF8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Oriya  (0B00..0B7F) */
        /* 0B00 */
        3840,
        3848,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0B08 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3840,
        3842,
        /* 0B10 */
        3842,
        3840,
        3840,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0B18 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0B20 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0B28 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0B30 */
        3841,
        3840,
        3841,
        3841,
        3840,
        3841,
        3841,
        3841,
        /* 0B38 */
        3841,
        3841,
        3840,
        3840,
        3843,
        3840,
        2823,
        1543,
        /* 0B40 */
        2823,
        2055,
        2055,
        2055,
        2055,
        3840,
        3840,
        775,
        /* 0B48 */
        1543,
        3840,
        3840,
        2823,
        2823,
        2052,
        3840,
        3840,
        /* 0B50 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        1543,
        2823,
        /* 0B58 */
        3840,
        3840,
        3840,
        3840,
        3841,
        3841,
        3840,
        3841,
        /* 0B60 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0B68 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0B70 */
        3840,
        3841,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0B78 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Tamil  (0B80..0BFF) */
        /* 0B80 */
        3840,
        3840,
        3848,
        3840,
        3840,
        3842,
        3842,
        3842,
        /* 0B88 */
        3842,
        3842,
        3842,
        3840,
        3840,
        3840,
        3842,
        3842,
        /* 0B90 */
        3842,
        3840,
        3842,
        3842,
        3842,
        3841,
        3840,
        3840,
        /* 0B98 */
        3840,
        3841,
        3841,
        3840,
        3841,
        3840,
        3841,
        3841,
        /* 0BA0 */
        3840,
        3840,
        3840,
        3841,
        3841,
        3840,
        3840,
        3840,
        /* 0BA8 */
        3841,
        3841,
        3841,
        3840,
        3840,
        3840,
        3841,
        3841,
        /* 0BB0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0BB8 */
        3841,
        3841,
        3840,
        3840,
        3840,
        3840,
        2823,
        2823,
        /* 0BC0 */
        1543,
        2055,
        2055,
        3840,
        3840,
        3840,
        775,
        775,
        /* 0BC8 */
        775,
        3840,
        2823,
        2823,
        2823,
        1540,
        3840,
        3840,
        /* 0BD0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        2823,
        /* 0BD8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0BE0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0BE8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0BF0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0BF8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Telugu  (0C00..0C7F) */
        /* 0C00 */
        3840,
        3848,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0C08 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3842,
        3842,
        /* 0C10 */
        3842,
        3840,
        3842,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0C18 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0C20 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0C28 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0C30 */
        3841,
        3841,
        3841,
        3841,
        3840,
        3841,
        3841,
        3841,
        /* 0C38 */
        3841,
        3841,
        3840,
        3840,
        3840,
        3840,
        1543,
        1543,
        /* 0C40 */
        1543,
        2823,
        2823,
        2823,
        2823,
        3840,
        1543,
        1543,
        /* 0C48 */
        2055,
        3840,
        1543,
        1543,
        1543,
        1540,
        3840,
        3840,
        /* 0C50 */
        3840,
        3840,
        3840,
        3840,
        3840,
        1543,
        2055,
        3840,
        /* 0C58 */
        3841,
        3841,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0C60 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0C68 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0C70 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0C78 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Kannada  (0C80..0CFF) */
        /* 0C80 */
        3840,
        3840,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0C88 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3842,
        3842,
        /* 0C90 */
        3842,
        3840,
        3842,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0C98 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0CA0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0CA8 */
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0CB0 */
        3841,
        3841,
        3841,
        3841,
        3840,
        3841,
        3841,
        3841,
        /* 0CB8 */
        3841,
        3841,
        3840,
        3840,
        3843,
        3840,
        2823,
        1543,
        /* 0CC0 */
        2823,
        2823,
        2823,
        2823,
        2823,
        3840,
        1543,
        2823,
        /* 0CC8 */
        2823,
        3840,
        2823,
        2823,
        1543,
        1540,
        3840,
        3840,
        /* 0CD0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        2823,
        2823,
        3840,
        /* 0CD8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3841,
        3840,
        /* 0CE0 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0CE8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0CF0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0CF8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Malayalam  (0D00..0D7F) */
        /* 0D00 */
        3840,
        3840,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0D08 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        3842,
        3842,
        /* 0D10 */
        3842,
        3840,
        3842,
        3842,
        3842,
        3841,
        3841,
        3841,
        /* 0D18 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0D20 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0D28 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0D30 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0D38 */
        3841,
        3841,
        3841,
        3840,
        3840,
        3840,
        2823,
        2823,
        /* 0D40 */
        2823,
        2823,
        2823,
        2055,
        2055,
        3840,
        775,
        775,
        /* 0D48 */
        775,
        3840,
        2823,
        2823,
        2823,
        1540,
        3855,
        3840,
        /* 0D50 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        2823,
        /* 0D58 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0D60 */
        3842,
        3842,
        2055,
        2055,
        3840,
        3840,
        3840,
        3840,
        /* 0D68 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0D70 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0D78 */
        3840,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* Sinhala  (0D80..0DFF) */
        /* 0D80 */
        3840,
        3840,
        3848,
        3848,
        3840,
        3842,
        3842,
        3842,
        /* 0D88 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        /* 0D90 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3840,
        /* 0D98 */
        3840,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0DA0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0DA8 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0DB0 */
        3841,
        3841,
        3840,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 0DB8 */
        3841,
        3841,
        3841,
        3841,
        3840,
        3841,
        3840,
        3840,
        /* 0DC0 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3840,
        /* 0DC8 */
        3840,
        3840,
        1540,
        3840,
        3840,
        3840,
        3840,
        2823,
        /* 0DD0 */
        2823,
        2823,
        1543,
        1543,
        2055,
        3840,
        2055,
        3840,
        /* 0DD8 */
        2823,
        775,
        1543,
        775,
        2823,
        2823,
        2823,
        2823,
        /* 0DE0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0DE8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 0DF0 */
        3840,
        3840,
        2823,
        2823,
        3840,
        3840,
        3840,
        3840,
        /* 0DF8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* Vedic Extensions  (1CD0..1CFF) */
        /* 1CD0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 1CD8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 1CE0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 1CE8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 1CF0 */
        3840,
        3840,
        3848,
        3848,
        3840,
        3840,
        3840,
        3840,
        /* 1CF8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
    ];
    public static $khmer_table = [
        /* Khmer  (1780..17FF) */
        /* 1780 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 1788 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 1790 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 1798 */
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        3841,
        /* 17A0 */
        3841,
        3841,
        3841,
        3842,
        3842,
        3842,
        3842,
        3842,
        /* 17A8 */
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        3842,
        /* 17B0 */
        3842,
        3842,
        3842,
        3842,
        3840,
        3840,
        2823,
        1543,
        /* 17B8 */
        1543,
        1543,
        1543,
        2055,
        2055,
        2055,
        1543,
        2823,
        /* 17C0 */
        2823,
        775,
        775,
        775,
        2823,
        2823,
        3848,
        3848,
        /* 17C8 */
        2823,
        3853,
        3853,
        3840,
        3855,
        3840,
        3840,
        3840,
        /* 17D0 */
        3840,
        1540,
        3844,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 17D8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 17E0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 17E8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 17F0 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        /* 17F8 */
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
        3840,
    ];
    // from "hb-ot-shape-complex-indic-table.cc"
    public static function indic_get_categories($u)
    {
        if (0x900 <= $u && $u <= 0xdff) {
            return self::$indic_table[$u - 0x900 + 0];
            // offset 0 for Most "indic"
        }
        if (0x1cd0 <= $u && $u <= 0x1d00) {
            return self::$indic_table[$u - 0x1cd0 + 1152];
            // offset for Vedic extensions
        }
        if (0x1780 <= $u && $u <= 0x17ff) {
            return self::$khmer_table[$u - 0x1780];
            // Khmer
        }
        if ($u == 0xa0) {
            return 3851;
            // (ISC_CP | (IMC_x << 8))
        }
        if ($u == 0x25cc) {
            return 3851;
            // (ISC_CP | (IMC_x << 8))
        }
        return 3840;
        // (ISC_x | (IMC_x << 8))
    }
    // BELOW from hb-ot-shape-complex-indic.cc
    /*
     * Indic shaper.
     */
    public static function IN_HALF_BLOCK($u, $Base)
    {
        return ($u & ~0x7f) == $Base;
    }
    public static function IS_DEVA($u)
    {
        return self::IN_HALF_BLOCK($u, 0x900);
    }
    public static function IS_BENG($u)
    {
        return self::IN_HALF_BLOCK($u, 0x980);
    }
    public static function IS_GURU($u)
    {
        return self::IN_HALF_BLOCK($u, 0xa00);
    }
    public static function IS_GUJR($u)
    {
        return self::IN_HALF_BLOCK($u, 0xa80);
    }
    public static function IS_ORYA($u)
    {
        return self::IN_HALF_BLOCK($u, 0xb00);
    }
    public static function IS_TAML($u)
    {
        return self::IN_HALF_BLOCK($u, 0xb80);
    }
    public static function IS_TELU($u)
    {
        return self::IN_HALF_BLOCK($u, 0xc00);
    }
    public static function IS_KNDA($u)
    {
        return self::IN_HALF_BLOCK($u, 0xc80);
    }
    public static function IS_MLYM($u)
    {
        return self::IN_HALF_BLOCK($u, 0xd00);
    }
    public static function IS_SINH($u)
    {
        return self::IN_HALF_BLOCK($u, 0xd80);
    }
    public static function IS_KHMR($u)
    {
        return self::IN_HALF_BLOCK($u, 0x1780);
    }
    public static function MATRA_POS_LEFT($u)
    {
        return self::POS_PRE_M;
    }
    public static function MATRA_POS_RIGHT($u)
    {
        return self::IS_DEVA($u) ? self::POS_AFTER_SUB : (self::IS_BENG($u) ? self::POS_AFTER_POST : (self::IS_GURU($u) ? self::POS_AFTER_POST : (self::IS_GUJR($u) ? self::POS_AFTER_POST : (self::IS_ORYA($u) ? self::POS_AFTER_POST : (self::IS_TAML($u) ? self::POS_AFTER_POST : (self::IS_TELU($u) ? $u <= 0xc42 ? self::POS_BEFORE_SUB : self::POS_AFTER_SUB : (self::IS_KNDA($u) ? $u < 0xcc3 || $u > 0xcd6 ? self::POS_BEFORE_SUB : self::POS_AFTER_SUB : (self::IS_MLYM($u) ? self::POS_AFTER_POST : (self::IS_SINH($u) ? self::POS_AFTER_SUB : (self::IS_KHMR($u) ? self::POS_AFTER_POST : self::POS_AFTER_SUB))))))))));
        /* default */
    }
    public static function MATRA_POS_TOP($u)
    {
        return self::IS_DEVA($u) ? self::POS_AFTER_SUB : (self::IS_GURU($u) ? self::POS_AFTER_POST : (self::IS_GUJR($u) ? self::POS_AFTER_SUB : (self::IS_ORYA($u) ? self::POS_AFTER_MAIN : (self::IS_TAML($u) ? self::POS_AFTER_SUB : (self::IS_TELU($u) ? self::POS_BEFORE_SUB : (self::IS_KNDA($u) ? self::POS_BEFORE_SUB : (self::IS_SINH($u) ? self::POS_AFTER_SUB : (self::IS_KHMR($u) ? self::POS_AFTER_POST : self::POS_AFTER_SUB))))))));
        /* default */
    }
    public static function MATRA_POS_BOTTOM($u)
    {
        return self::IS_DEVA($u) ? self::POS_AFTER_SUB : (self::IS_BENG($u) ? self::POS_AFTER_SUB : (self::IS_GURU($u) ? self::POS_AFTER_POST : (self::IS_GUJR($u) ? self::POS_AFTER_POST : (self::IS_ORYA($u) ? self::POS_AFTER_SUB : (self::IS_TAML($u) ? self::POS_AFTER_POST : (self::IS_TELU($u) ? self::POS_BEFORE_SUB : (self::IS_KNDA($u) ? self::POS_BEFORE_SUB : (self::IS_MLYM($u) ? self::POS_AFTER_POST : (self::IS_SINH($u) ? self::POS_AFTER_SUB : (self::IS_KHMR($u) ? self::POS_AFTER_POST : self::POS_AFTER_SUB))))))))));
        /* default */
    }
    public static function matra_position($u, $side)
    {
        switch ($side) {
            case self::POS_PRE_C:
                return self::MATRA_POS_LEFT($u);
            case self::POS_POST_C:
                return self::MATRA_POS_RIGHT($u);
            case self::POS_ABOVE_C:
                return self::MATRA_POS_TOP($u);
            case self::POS_BELOW_C:
                return self::MATRA_POS_BOTTOM($u);
        }
        return $side;
    }
    // vowel matras that have to be split into two parts.
    // From Harfbuzz (old)
    // New HarfBuzz uses /src/hb-ucdn/ucdn.c and unicodedata_db.h for full method of decomposition for all characters
    // Should always fully decompose and then recompose back, but we will just do the split matras
    public static function decompose_indic($ab)
    {
        $sub = [];
        switch ($ab) {
            /*
             * Decompose split matras.
             */
            /* bengali */
            case 0x9cb:
                $sub[0] = 0x9c7;
                $sub[1] = 0x9be;
                return $sub;
            case 0x9cc:
                $sub[0] = 0x9c7;
                $sub[1] = 0x9d7;
                return $sub;
            /* oriya */
            case 0xb48:
                $sub[0] = 0xb47;
                $sub[1] = 0xb56;
                return $sub;
            case 0xb4b:
                $sub[0] = 0xb47;
                $sub[1] = 0xb3e;
                return $sub;
            case 0xb4c:
                $sub[0] = 0xb47;
                $sub[1] = 0xb57;
                return $sub;
            /* tamil */
            case 0xbca:
                $sub[0] = 0xbc6;
                $sub[1] = 0xbbe;
                return $sub;
            case 0xbcb:
                $sub[0] = 0xbc7;
                $sub[1] = 0xbbe;
                return $sub;
            case 0xbcc:
                $sub[0] = 0xbc6;
                $sub[1] = 0xbd7;
                return $sub;
            /* telugu */
            case 0xc48:
                $sub[0] = 0xc46;
                $sub[1] = 0xc56;
                return $sub;
            /* kannada */
            case 0xcc0:
                $sub[0] = 0xcbf;
                $sub[1] = 0xcd5;
                return $sub;
            case 0xcc7:
                $sub[0] = 0xcc6;
                $sub[1] = 0xcd5;
                return $sub;
            case 0xcc8:
                $sub[0] = 0xcc6;
                $sub[1] = 0xcd6;
                return $sub;
            case 0xcca:
                $sub[0] = 0xcc6;
                $sub[1] = 0xcc2;
                return $sub;
            case 0xccb:
                $sub[0] = 0xcc6;
                $sub[1] = 0xcc2;
                $sub[2] = 0xcd5;
                return $sub;
            /* malayalam */
            case 0xd4a:
                $sub[0] = 0xd46;
                $sub[1] = 0xd3e;
                return $sub;
            case 0xd4b:
                $sub[0] = 0xd47;
                $sub[1] = 0xd3e;
                return $sub;
            case 0xd4c:
                $sub[0] = 0xd46;
                $sub[1] = 0xd57;
                return $sub;
            /* sinhala */
            // NB Some fonts break with these Sinhala decomps (although this is Uniscribe spec)
            // Can check if character would be substituted by pstf and only decompose if true
            // e.g. if (isset($GSUBdata['pstf'][$ab])) - would need to pass $GSUBdata as parameter to this function
            case 0xdda:
                $sub[0] = 0xdd9;
                $sub[1] = 0xdca;
                return $sub;
            case 0xddc:
                $sub[0] = 0xdd9;
                $sub[1] = 0xdcf;
                return $sub;
            case 0xddd:
                $sub[0] = 0xdd9;
                $sub[1] = 0xdcf;
                $sub[2] = 0xdca;
                return $sub;
            case 0xdde:
                $sub[0] = 0xdd9;
                $sub[1] = 0xddf;
                return $sub;
            /* khmer */
            case 0x17be:
                $sub[0] = 0x17c1;
                $sub[1] = 0x17be;
                return $sub;
            case 0x17bf:
                $sub[0] = 0x17c1;
                $sub[1] = 0x17bf;
                return $sub;
            case 0x17c0:
                $sub[0] = 0x17c1;
                $sub[1] = 0x17c0;
                return $sub;
            case 0x17c4:
                $sub[0] = 0x17c1;
                $sub[1] = 0x17c4;
                return $sub;
            case 0x17c5:
                $sub[0] = 0x17c1;
                $sub[1] = 0x17c5;
                return $sub;
            /* tibetan - included here although does not use Inidc shaper in other ways  */
            case 0xf73:
                $sub[0] = 0xf71;
                $sub[1] = 0xf72;
                return $sub;
            case 0xf75:
                $sub[0] = 0xf71;
                $sub[1] = 0xf74;
                return $sub;
            case 0xf76:
                $sub[0] = 0xfb2;
                $sub[1] = 0xf80;
                return $sub;
            case 0xf77:
                $sub[0] = 0xfb2;
                $sub[1] = 0xf81;
                return $sub;
            case 0xf78:
                $sub[0] = 0xfb3;
                $sub[1] = 0xf80;
                return $sub;
            case 0xf79:
                $sub[0] = 0xfb3;
                $sub[1] = 0xf71;
                $sub[2] = 0xf80;
                return $sub;
            case 0xf81:
                $sub[0] = 0xf71;
                $sub[1] = 0xf80;
                return $sub;
        }
        return \false;
    }
    public static function bubble_sort(&$arr, $start, $len)
    {
        if ($len < 2) {
            return;
        }
        $k = $start + $len - 2;
        while ($k >= $start) {
            for ($j = $start; $j <= $k; $j++) {
                if ($arr[$j]['indic_position'] > $arr[$j + 1]['indic_position']) {
                    $t = $arr[$j];
                    $arr[$j] = $arr[$j + 1];
                    $arr[$j + 1] = $t;
                }
            }
            $k--;
        }
    }
}
