<?php

/**
 * Copyright (c) NMS PRIME GmbH ("NMS PRIME Community Version")
 * and others – powered by CableLabs. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace App\Http\Controllers;

use Auth;
use BaseModel;
use Bouncer;
use Form;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
use Module;
use Request;
use Route;
use Session;
use Spatie\Html\Elements\Div;
use Str;

/*
 * BaseViewController: Is a special Controller which will be a kind of middleware/sub-layer/helper
 *                     between the classical Controllers and Views.

 * Purpose: This Controller is manly used to reduce the logical hard code php stuff from generic views
 *          and to bring the view related stuff from BaseController to a better place - BaseViewController.
 *          This leads to a kind of theoretical sub-layer concept – in fact it is not! See later ..
 *
 * At the time it is not a full qualified sub-layer (API) in this manner that all stuff goes through this
 * Controller, whats between all Controllers and Views. It's more a kind of "Helper" to increase the logical
 * sructuring. This has the advantage that we do not need a complete re-write.

 * Usage: Most of the function here are used in a simple static context from BaseController like
 *        BaseViewController::do_prepare_view_xyz().
 *
 * @author: Torsten Schmidt
 */
class BaseViewController extends Controller
{
    /**
     * Searches for a string in the language files under resources/lang/ and returns it for the active application language
     * Searches for a "*" (required field), deletes it for trans function and appends it at the end
     * used in everything Form related (Labels, descriptions)
     *
     * @author Nino Ryschawy, Christian Schramm
     */
    public static function translate_label($string)
    {
        // cut the star at the end of value if there is one for the translate function and append it after translation
        $star = '';
        if (strpos($string, '*')) {
            $string = str_replace(' *', '', $string);
            $star = ' *';
        }

        if (strpos($string, 'messages.') !== false) {
            return trans($string).$star;
        }

        $translation = trans("messages.$string");

        // found in lang/{}/messages.php
        if (strpos($translation, 'messages.') === false) {
            return $translation.$star;
        }

        return $string.$star;
    }

    /**
     * Searches for a string in the language files under resources/lang/ and returns it for the active application language
     * used in everything view related
     *
     * @param string:   string that is searched in resspurces/lang/{App-language}/view.php
     * @param type:     can be Header, Menu, Button, jQuery, Search
     * @param count:    standard at 1 , For plural translation - needs to be seperated with pipe "|""
     *                  example: Index Headers -> in view.php: 'Header_Mta' => 'MTA|MTAs',
     *
     * @author Christian Schramm
     */
    public static function translate_view($string, $type, $count = 1)
    {
        if (strpos($string, 'view.'.$type.'_')) {
            return trans($string);
        }

        $translation = trans_choice('view.'.$type.'_'.$string, $count);

        // found in lang/{}/messages.php
        if (strpos($translation, 'view.'.$type.'_') === false) {
            return $translation;
        }

        return $string;
    }

    /**
     * Translate every value of a given Array
     *
     * @param  array  $array  or iterable
     * @param  string  $file
     * @return Illuminate\Support\Collection
     */
    public static function translateArray($array, $file = 'messages')
    {
        return collect($array)->map(function ($string) use ($file) {
            return trans("{$file}.{$string}");
        });
    }

    /**
     * Get all two letter language strings as array
     *
     * @return Illuminate\Support\Collection
     */
    public static function getAllLanguages()
    {
        return collect(glob(app()->langPath().'/*'))
            ->mapWithKeys(function ($path) {
                $langShortcut = basename($path);

                return [$langShortcut => $langShortcut];
            });
    }

    /**
     * Get translation of class name
     *
     * @return string
     */
    public static function getTranslatedClassName($class, $count = 1)
    {
        // Look in view.php first
        if (trans("view.Header_{$class}") != "view.Header_{$class}") {
            return trans_choice("view.Header_{$class}", $count ?: 1);
        }

        return trans("messages.{$class}");
    }

    /**
     * Generate the Array for Language selection in select fields
     *
     * @param  array  $languageArray  or iterable
     * @return Illuminate\Support\Collection
     */
    public static function generateLanguageArray($languageArray)
    {
        return collect($languageArray)
            ->mapWithKeys(function ($langShortcut) {
                return [$langShortcut  => config('language.'.$langShortcut)];
            });
    }

    /**
     * Detect the user language from Session or browser
     *
     * @return string
     */
    public static function get_user_lang(): string
    {
        if (Session::has('language')) {
            return Session::get('language');
        }

        $user = Auth::user();

        if (! $user || $user->language == 'browser') {
            // check the Browser for the accepted language
            return isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ?
                checkLocale(substr(explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE'])[0], 0, 2)) :
                checkLocale();
        }

        Session::put('language', $userLang = checkLocale($user->language));

        return  $userLang;
    }

    /**
     * This function is used to prepare the resulting view_form_field array for edit view.
     * So all general preparation stuff to view_form_fields() will be done here.
     *
     * Tasks:
     *  1. Add a (*) to fields description if validation rule contains required
     *  2. Add Placeholder YYYY-MM-DD for all date fields
     *  3. Hide all parent view relation select fields (works only in edit context)
     *  4. auto-fill field_value with correlating model data (from sql)
     *  5. IP online check for form_type = 'ip' || 'ping'
     *
     * @param fields: the view_form_fields  []
     * @param model: the model to view. Note: could be get_model_obj()->find($id) or get_model_obj()
     *
     * @return: the modifeyed view_form_fields  []
     *
     * @author: Torsten Schmidt
     */
    public static function prepare_form_fields($fields, $model)
    {
        $ret = [];

        // get the validation rules for related model object
        $rules = $model->rules();
        $view_belongs_to = $model->view_belongs_to();

        // for all fields
        foreach ($fields as $field) {
            /**
             * Normalize the field name to make it compatible with
             * array accessors e.g.
             * tags[] =>  tags
             * custom_data[foo] => custom_data.foo
             * name_without_brackets =>  name_without_brackets
             *
             * `dotify(...)` is a macro and not Laravel's native helper
             *
             * @see \App\Providers\AppServiceProvider
             */
            $field_name = (string) str($field['name'])->dotify();

            if ($field['form_type'] === 'collapse') {
                if (Str::endsWith(Route::currentRouteName(), '.api_create')) {
                    // API: show all possible fields in a flat layout
                    $ret = array_merge($ret, self::prepare_form_fields($field['form_fields'], $model));
                } else {
                    // GUI: hide / collapse some field (e.g. number{2,3,4} of Contract)
                    $field['form_fields'] = self::prepare_form_fields($field['form_fields'], $model);
                    array_push($ret, $field);
                }
                continue;
            }

            // rule exists for actual field ?
            if ($fieldRules = $rules[$field_name] ?? null) {
                $fieldRules = is_array($fieldRules) ? $fieldRules : explode('|', $fieldRules);

                // 1. Add a (*) to fields description if validation rule contains required
                if (in_array('required', $fieldRules)) {
                    $field['description'] = $field['description'].' *';

                    // Add the HTML required attribute to avoid an unecessay server round trip
                    data_fill($field, 'options.required', true);
                }

                // 2. Add Placeholder YYYY-MM-DD for all date fields if not yet set
                if (in_array('date', $fieldRules) && ! isset($field['options']['placeholder'])) {
                    $field['options']['placeholder'] = 'YYYY-MM-DD';
                }
            }

            // 3. Hide all parent view relation select fields (in edit context)
            //    NOTE: this will not work in create context, because view_belongs_to() returns null !
            //          Hiding in create context will only work with hard coded 'hidden' => 1 entry in view_form_fields()
            if (
                // does a view relation exists?
                is_object($view_belongs_to) &&
                // not a n:m relation (in which case we have an pivot table)
                (! ($view_belongs_to instanceof \Illuminate\Support\Collection)) &&
                // view table name (*_id) == field name ?
                ($view_belongs_to->table.'_id' == $field['name']) &&
                // hidden was not explicitly set
                (! isset($field['hidden']))
            ) {
                $field['hidden'] = '1';
            }

            // 4. set all field_value's to actual SQL data
            if (array_key_exists('eval', $field)) {
                // dont't remove $name, as it is used in $field['eval'] (might be set in view_form_fields())
                $name = $model[$field['name']];
                $eval = $field['eval'];
                $field['field_value'] = eval("return $eval;");
            } else {
                $field['field_value'] = data_get($model, $field_name);
            }

            // PATCH: Allow pre-filling array fields (e.g. custom_data[addressId]) from GET/POST using dot notation
            $field_value_from_request = \Request::input(str($field['name'])->dotify(), null);
            if (! is_null($field_value_from_request)) {
                $field['field_value'] = $field_value_from_request;
            }

            // NOTE: Request::get should actually include $_POST global var and $_GET!!
            // 4.(sub-task) auto-fill all field_value's with HTML POST array if supposed
            if (isset($_POST[$field['name']])) {
                $field['field_value'] = $_POST[$field['name']];
            }

            // 4. (sub-task)
            // write explicitly given init_value to field_value
            // this is needed e.g. by Patrick to prefill new PhonenumberManagement and PhonebookEntry with data from Contract
            if (isset($field['init_value']) && ! $field['field_value']) {
                $field['field_value'] = $field['init_value'];
            }

            // 5. ip online check
            if ($field['form_type'] == 'ip') {
                $ping = $offline = null;

                // Ping: Only check if ip is online
                if ($model[$field['name']]) {
                    $cmd = 'ping';
                    // $model[$field['name']] is null e.g. on NetGw/create
                    $host = explode(':', $model[$field['name']])[0];

                    // IPv6
                    if (filter_var($model[$field['name']], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                        $cmd .= ' -6';
                        $host = $model[$field['name']];
                    }

                    exec("sudo $cmd -c1 -i0 -w1 ".$host, $ping, $offline);

                    if ($offline) {
                        $field['help'] = 'Device seems to be Offline!';
                        $field['help_icon'] = 'fa-exclamation-triangle text-warning';
                    } else {
                        $model->isOnline = true;
                        $field['help'] = 'Device is Online';
                        $field['help_icon'] = 'fa-check-circle-o text-success';
                    }
                } else {
                    // there is no device to ping – so we do not provide info about online status
                    $field['help'] = '';
                    $field['help_icon'] = '';
                }

                $field['form_type'] = 'text';
            }

            // 6. prepare autocomplete field
            if (isset($field['autocomplete']) && is_array($field['autocomplete'])) {
                if (count($field['autocomplete']) === 0) {
                    $field['autocomplete'][] = explode('.', Route::currentRouteName())[0];
                }
                if (count($field['autocomplete']) === 1) {
                    $field['autocomplete'][] = $field['name'];
                }
            } else {
                unset($field['autocomplete']);
            }

            array_push($ret, $field);
        }

        return $ret;
    }

    /**
     * Add ['html'] element to each $fields entry
     *
     * The html element contains the HTML formated code to display each HTML field.
     * You could use 'html' parameter inside the view_form_fields() functions to
     * overwrite default behavior. The best advice to use these parameter is to
     * debug the return array of this function and adapt it to you requirements.
     *
     * @param fields: the prepared view_form_fields  [], each array element represents on (HTML) field
     * @param context: edit|create - context from which this function is called
     *
     * @return: [] of fields with added ['html'] element containing the preformed html content
     *
     * @author: Torsten Schmidt
     */
    public static function add_html_string($fields, $context = 'edit')
    {
        // background color's to toggle through
        $colorArray = ['bg-whitesmoke', 'bg-gainsboro'];
        $color = $colorArray[0];

        $formular = [];

        // foreach fields
        foreach ($fields as $field) {
            $currentFormfield = [];
            // hidden stuff
            if (array_key_exists('hidden', $field) &&
                $field['hidden'] != 0 && ( // == 0 -> explicitly set to always show, no matter if other conditions are met
                    ($context == 'edit' && strpos($field['hidden'], 'E') !== false) || // hide edit context only?
                    ($context == 'create' && strpos($field['hidden'], 'C') !== false) || // hide create context only?
                    $field['hidden'] == 1) // hide globally?
            ) {
                [$currentFormfield[], $color, $colorArray] = self::handleHiddenField($field, $colorArray);
                $temp = $field;
                $temp['html'] = $currentFormfield;
                $formular[] = $temp;

                continue;
            }

            // prepare value and options vars
            $value = isset($field['value']) ? $field['value'] : [];
            $options = isset($field['options']) ? $field['options'] : [];

            // field color
            if (! isset($options['style'])) {
                $options['style'] = '';
            }

            if (! isset($options['class'])) {
                $options['class'] = '';
            }
            $options['class'] .= $options['style'] == 'simple' ? '' : " bg-$color dark:bg-slate-800 dark:text-slate-100";

            // Help: add help msg to form fields - mouse on hover
            if (isset($field['help'])) {
                $options['title'] = $field['help'];
            }

            // select field: used for jquery (java script) realtime based showing/hiding of fields
            $select = null;
            if (isset($field['select']) && is_string($field['select'])) {
                $select = ['class' => $field['select']];
            }

            // checkbox field: used for jquery (java script) realtime based showing/hiding of fields
            $checkbox = null;
            if (isset($field['checkbox']) && is_string($field['checkbox'])) {
                $checkbox = ['class' => $field['checkbox']];
            }

            // combine the classes to trigger show/hide from select and checkbox
            // the result is either null or an array containing the classes in key 'class'
            $additional_classes = $select;
            if (is_array($additional_classes)) {
                if (is_array($checkbox)) {
                    $additional_classes['class'] .= ' '.$checkbox['class'];
                }
            } else {
                $additional_classes = $checkbox;
            }

            // Open Form Group
            if (! in_array($field['form_type'], ['collapse'])) {
                $currentFormfield[] = Form::openGroup($field['name'], $field['description'], $additional_classes, $color);
            }

            // Output the Form Elements
            switch ($field['form_type']) {
                case 'checkbox':
                    // Checkbox - where pre-checked is enabled
                    if ($value == []) {
                        $value = 1;
                    }

                    if ($context == 'create') {
                        // only take care of checked statement if we are called in context create
                        $checked = (isset($field['checked'])) ? $field['checked'] : $field['field_value'];
                    } else {
                        $checked = $field['field_value'];
                    }

                    $currentFormfield[] = Form::checkbox($field['name'], $value, null, $checked, $options);
                    break;

                case 'file':
                    $currentFormfield[] = Form::file($field['name'], $options);
                    break;

                case 'select':
                    if (isset($options['multiple']) && isset($field['selected'])) {
                        $escaped_field_name = Str::substr($field['name'], 0, Str::length($field['name']) - 2);
                        $field['field_value'] = Request::old($escaped_field_name, array_keys($field['selected']));
                        // values MUST be int, because of strict type checking in Form module
                        // $field['field_value'] = array_map('intval', $field['field_value']);
                    }

                    $currentFormfield[] = Form::select($field['name'], $value, $field['field_value'], $options, $field['optionsAttributes'] ?? []);
                    break;

                case 'password':
                    $currentFormfield[] = Form::password($field['name'], $options);
                    break;

                case 'number':
                    $currentFormfield[] = Form::number($field['name'], $field['field_value'], ...Arr::only($options, ['min', 'max', 'step']));
                    break;

                case 'link':
                    $currentFormfield[] = Form::link($field['name'], $field['url'], isset($field['color']) ?: 'default');
                    break;

                case 'html':
                    $currentFormfield[] = new HtmlString($field['html']);
                    break;

                case 'date':
                    $options['onchange'] = "$('#{$field['name']}')[0].defaultValue = event.target.value;$('#{$field['name']}')[0].value = event.target.value;";
                    $options['autocomplete'] = 'off';

                    $currentFormfield[] = Form::date($field['name'], $field['field_value'], $options);
                    break;

                case 'time':
                    $options['onchange'] = "$('#{$field['name']}')[0].defaultValue = event.target.value;$('#{$field['name']}')[0].value = event.target.value;";
                    $options['autocomplete'] = 'off';

                    $field['field_value'] = ! empty($field['field_value']) && strtotime($field['field_value'])
                        ? date('H:i', strtotime($field['field_value']))
                        : null;

                    $currentFormfield[] = Form::time($field['name'], $field['field_value'], $options, false);
                    break;

                case 'collapse':
                    $currentFormfield[] = new HtmlString(view('Components.collapse', [
                        'color' => $color,
                        'innerFields' => self::add_html_string($field['form_fields']),
                        'field' => $field,
                    ])->render());
                    break;

                default:
                    $form = $field['form_type'];
                    $options[] = 'v-pre';
                    $currentFormfield[] = Form::$form($field['name'], $field['field_value'], $options);
                    break;
            }

            // Help: add help icon/image behind form field
            if (isset($field['help'])) {
                $currentFormfield[] = self::helpIcon($field);
            }

            // Close Form Group
            if (! in_array($field['form_type'], ['collapse'])) {
                $currentFormfield[] = Form::closeGroup();
            }

            if (array_key_exists('space', $field)) {
                [$currentFormfield, $color, $colorArray] = self::addSpacer($currentFormfield, $colorArray);
            }

            $temp = $field;
            $temp['html'] = $currentFormfield;
            $formular[] = $temp;
        }

        return $formular;
    }

    // Space Element between fields and color switching
    protected static function addSpacer($currentFormfield, $colorArray)
    {
        $colorArray = \App\extensions\php\ArrayHelper::array_rotate($colorArray);

        return [
            [$currentFormfield, Div::create()->class('col-12 mb-6')],
            $colorArray[0],
            $colorArray,
        ];
    }

    protected static function handleHiddenField($field, $colorArray)
    {
        // For hidden fields it's also important that default values are set
        $value = ($field['field_value'] === null) && isset($field['value']) ? $field['value'] : $field['field_value'];
        // Note: setting a selection by giving an array doesnt make sense as you can not choose anyway - it also would throw an exception as it's not allowed for hidden fields
        $currentFormfield = Form::hidden($field['name'], is_array($value) ? '' : $value);

        if (array_key_exists('space', $field)) {
            [$currentFormfield, $color, $colorArray] = self::addSpacer($currentFormfield, $colorArray);
        }

        return [$currentFormfield, $color ?? $colorArray[0], $colorArray];
    }

    /**
     * Add simple html input String to the Fields-Array  -- without label - for use in HTML Tables
     */
    public static function get_html_input($field)
    {
        $s = '';

        $value = isset($field['value']) ? $field['value'] : [];
        $options = isset($field['options']) ? $field['options'] : [];

        switch ($field['form_type']) {
            case 'checkbox':
                // Checkbox - where pre-checked is enabled
                if ($value == []) {
                    $value = 1;
                }

                $s .= Form::checkbox($field['name'], $value, null, $field['field_value']);
                break;

            case 'select':
                $s .= Form::select($field['name'], $value, $field['field_value'], [], $options);
                break;

            case 'password':
                $s .= Form::password($field['name']);
                break;

            case 'link':
                $s .= Form::link($field['name'], $field['url'], isset($field['color']) ?: 'default');
                break;

            default:
                if (in_array('readonly', $options)) {
                    return '<p name="'.$field['name'].'">'.$field['field_value'].'</p>';
                }

                $form = $field['form_type'];
                $s .= Form::$form($field['name'], $field['field_value'], $options);
                break;
        }

        return $s;
    }

    /**
     * Compose HTML of help icon from form field
     *
     * @param  array  $field  Entry of view_form_fields()
     * @return string
     */
    public static function helpIcon($field)
    {
        $bsClass = $field['color'] ?? 'info';
        $title = isset($field['description']) ? self::translate_label($field['description']) : '';
        $icon = $field['help_icon'] ?? 'fa-question-circle';

        return new HtmlString(view('Components.help-icon', compact('bsClass', 'field', 'icon', 'title'))->render());
    }

    /**
     * Get HTML string for geoposition fields to use for html key in view_form_fields()
     * (e.g. Modem, Node, Realty)
     *
     * @return string
     */
    public static function geoPosFields($model)
    {
        return view('Components.geopos-field', compact('model'))->render();
    }

    /**
     * This Method returns The Menuobjects for the Main Menu which constist of
     * icon, link and class of the page.
     *
     * NOTE: this function takes care of installed modules and Permissions!
     *
     * @return array
     *
     * @author: Christian Schramm
     */
    public static function view_main_menus(): array
    {
        if (Session::has('menu') && Auth::user()->language !== 'browser') {
            return Session::get('menu');
        }

        $menu = [];
        $configMenuItemKey = 'MenuItems';

        $menu['Global']['icon'] = 'fa-globe';
        $menu['Global']['link'] = config('base.link');
        $menu['Global']['translated_name'] = trans('view.Global');

        foreach (config('base.'.$configMenuItemKey) as $page => $settings) {
            if (Bouncer::can('view', $settings['class'])) {
                $menuItem = static::translate_view($page, 'Menu');
                $menu['Global']['submenu'][$menuItem] = $settings;
            }
        }

        $modules = Module::allEnabled();
        foreach ($modules as $name => $module) {
            $config = config($module->getLowerName());

            if (! isset($config[$configMenuItemKey])) {
                continue;
            }

            if (! empty($config['parent'])) {
                $module = $modules[$config['parent']];
                $name = $module->getName();
            }

            $menu[$name]['icon'] = $module->get('icon');
            $menu[$name]['link'] = $config['link'] ?? null;
            $menu[$name]['translated_name'] = static::translate_view($name, 'Menu');

            if (empty($config[$configMenuItemKey])) {
                continue;
            }

            foreach ($config[$configMenuItemKey] as $page => $settings) {
                if (Bouncer::can('view', $settings['class'])) {
                    $menuItem = static::translate_view($page, 'Menu');
                    $menu[$name]['submenu'][$menuItem] = $settings;
                }
            }
        }

        Session::put('menu', $menu);

        return $menu;
    }

    /**
     * This is a local helper to be able to show HTML code (like images) in breadcrumb
     *
     * @author: Torsten Schmidt
     *
     * @todo: move to a generic helper class
     */
    private static function linkRouteHtml($name, $title = null, $parameters = [], string $class = '')
    {
        return html()->a(route($name, $parameters), $title)->class($class)->render();
    }

    /**
     * Get the ICON of the class or object or from actual context
     *
     * @param  $class_or_obj:  the class or object to look for the icon
     * @return the HTML icon (with HTML tags)
     *
     * @author: Torsten Schmidt
     */
    public static function __get_view_icon($class_or_obj)
    {
        // NOTE: this does the trick: fetch the image when no object
        //       is present, like on create page
        $class = \NamespaceController::get_model_name();

        if (is_object($class_or_obj)) {
            $class = get_class($class_or_obj);
        }

        if (class_exists($class_or_obj)) {
            $class = $class_or_obj;
        }

        return new HtmlString($class::view_icon());
    }

    /**
     * Generate Top Header Link (like e.g. Contract > Modem > Mta > ..)
     * Shows the html links of the related objects recursively
     *
     * @param  $route_name:  route name of actual controller
     * @param  $view_header:  the view header name
     * @param  $view_var:  the object to generate the link from
     * @param  $html:  the HTML GET array. See note bellow!
     * @return the HTML link line to be directly included in blade
     *
     * @author Torsten Schmidt, Patrick Reichel
     *
     * NOTE: in create context we are forced to work with HTML GET array in $html.
     *       The first request will also work with POST array, but if validation fails
     *       there is no longer any POST array we can work with. Note that POST array is
     *       generated in relation.blade.
     *
     *       To avoid this we must ensure that every relational create send it's correlating
     *       model key, like contract_id=xyz in HTML GET request.
     */
    public static function compute_headline($route_name, $view_header, $view_var, $html = null)
    {
        $breadcrumb_path = '';
        $breadcrumb_paths = [];

        // only for create context: parse headline from HTML POST context array
        if (! is_null($html) && isset(array_keys($html)[0])) {
            $key = array_keys($html)[0];
            $fullyQualifiedClassName = (new BaseModel())->guessModelName(ucwords(explode('_id', $key)[0]));

            if (class_exists($fullyQualifiedClassName)) {
                $view_var = $fullyQualifiedClassName::find(intval($html[$key]));
            }
        }

        if ($view_var != null) {
            $parent = $view_var;
            $i = 0;
            while ($parent) {
                if (
                    (! ($parent instanceof \Illuminate\Support\Collection)) || // if $parent is not a Collection we have a 1:1 or 1:n relation
                    ($parent->count() == 1)                     // there is a potential n:m relation, but only one model is really connected
                ) {
                    // this means we have an explicit next step in our breadcrumb path

                    // if we got a collection we first have to extract the model
                    if ($parent instanceof \Illuminate\Support\Collection) {
                        $parent = $parent->pop();
                    }

                    // add the current model to breadcrumbs
                    $breadcrumb_path = self::extendBreadcrumbPath($breadcrumb_path, $parent, $i);

                    // get view parent
                    $parent = $parent->view_belongs_to();
                    $i++;
                } else {
                    // $parent is a collection with more than one entry – this means we have a multiple parents
                    // for example Phonenumber:EnviaOrder is such a n:m relatio
                    // we show breadcrumb paths for all of them, but then stopping further processing
                    // to avoid shredding the layout
                    $breadcrumb_path_before_split = $breadcrumb_path;
                    $multicrumbs = '';

                    foreach ($parent as $p) {
                        // get the breadcrumb for the current parent
                        $extended_path = self::extendBreadcrumbPath($breadcrumb_path, $p, $i);
                        $breadcrumb = str_replace($breadcrumb_path_before_split, '', $extended_path);

                        // overwrite style (default needs to much space on page)
                        $new_style = [
                            'font-size: 80%',
                            'display: block !important',
                            'padding-top: 0px !important',
                            'padding-bottom: 1px !important',
                        ];
                        $new_style = implode('; ', $new_style);
                        $breadcrumb = str_replace('<a ', '<a style="'.$new_style.'" ', $breadcrumb);

                        // collect all parent breadcrumbs
                        if (! $multicrumbs) {
                            // the first one is simple :-)
                            $multicrumbs = $breadcrumb;
                        } else {
                            // insert the current breadcrumb into the existing <li> element

                            // therefore we extract all text from the first opening to the last closing <a> tag
                            // from the current breadcrumb
                            $pattern_a_tag = '#\<a .*\</a\>#';
                            preg_match($pattern_a_tag, $breadcrumb, $matches);
                            $stripped_breadcrumb = $matches[0];

                            // then we extract the same pattern from the collected breadcrumbs
                            // and replace it by itself (backreference) extended by our cleaned current breadcrumb
                            $replacement = '$0'.$stripped_breadcrumb;
                            $multicrumbs = preg_replace($pattern_a_tag, $replacement, $multicrumbs);
                        }
                    }

                    // add all the parent breadcrumbs as a single breadcrumb path element
                    array_push($breadcrumb_paths, $multicrumbs.$breadcrumb_path_before_split);

                    // don't add more predecessors as this can really shred the layout
                    // (think about cascading n:m relations…)
                    break;
                }
            }
        }

        // Base Link to Index Table in front of all relations
        if (in_array($route_name, BaseController::get_config_modules())) {  // parse: Global Config requires own link
            $breadcrumb_path_base = "<div class='flex'><div class='flex flex-col py-1 px-2.5 text-slate-100 rounded bg-slate-800 hover:bg-slate-900'>".static::linkRouteHtml('Config.index', static::__get_view_icon($view_var).self::translate_view('Global Configurations', 'Header'), [], 'text-white hover:text:white no-underline').'</div></div>';
        } else {
            $breadcrumb_path_base = Route::has($route_name.'.index') ? "<div class='flex items-center'><div class='flex flex-col py-1 !px-3 text-slate-100 rounded bg-slate-800 hover:bg-slate-900'>".static::linkRouteHtml($route_name.'.index', static::__get_view_icon($view_var).Str::limit($view_header, 40), [], 'text-white hover:text:white no-underline').'</div></div>' : '';
        }

        if (! $breadcrumb_paths) {  // if this array is still empty: put the one and only breadcrumb path in this array
            array_push($breadcrumb_paths, $breadcrumb_path_base.$breadcrumb_path);
        } else {    // multiple breadcrumb paths: show overture on a single line
            array_unshift($breadcrumb_paths, $breadcrumb_path_base);
        }

        return implode('', $breadcrumb_paths);
    }

    private static function extendBreadcrumbPath($breadcrumb_path, $model, $i)
    {
        if (! $model) {
            return '';
        }

        // following is the original source code written by Torsten
        $tmp = explode('\\', get_class($model));
        $view = end($tmp);

        // get header field name
        // NOTE: for historical reasons check if this is a array or a plain string
        // See: Confluence API  - get_view_headline()
        $name = static::__get_view_icon($model);
        if (is_array($model->view_index_label())) {
            $name .= Str::limit($model->view_index_label()['header'], 40);
        } else {
            $name .= Str::limit($model->view_index_label(), 40);
        }

        if ($i == 0) {
            $breadcrumb_path = "<div class='flex items-center'><div class='w-2 h-full rounded-full bg-".$model->get_bsclass()."'></div><div class='flex flex-col px-2.5 text-black dark:text-slate-100'>".static::linkRouteHtml($view.'.edit', self::translate_view($name, 'Header'), $model->id).$breadcrumb_path.'</div></div>';
        } else {
            $breadcrumb_path = "<div class='flex items-center'><div class='w-2 h-full rounded-full bg-".$model->get_bsclass()."'></div><div class='flex flex-col px-2.5 text-black dark:text-slate-100'>".static::linkRouteHtml($view.'.edit', self::translate_view($name, 'Header'), $model->id).'</div></div>'.$breadcrumb_path;
        }

        return $breadcrumb_path;
    }

    /*
     * Return the API Version of view_has_many() as normal incremental integer
     *
     * @param view_has_many_array: the returned view_has_many() array
     * @return: api version starting from 1, 2, ..
     *
     * @autor: Torsten Schmidt
     */
    public static function get_view_has_many_api_version($view_has_many_array)
    {
        if (\App\extensions\php\ArrayHelper::array_depth($view_has_many_array) < 2) {
            return 1;
        }

        return 2;
    }

    /*
     * Prepare Index Entry Table (<tr>) Colors
     *
     * @param $object: the object to look at
     * @param $rotate_after: rotate color array after number of entries
     * @return: bootstrap table index class/color [success|warning|danger|info]
     *
     * @autor: Torsten Schmidt
     */
    public static function prep_index_entries_color($object, $rotate_after = 5)
    {
        static $colorArray = ['success', 'warning', 'danger', 'info'];
        static $i;

        $class = current($colorArray);

        // Check if class object has a own color definition
        if (isset($object->view_index_label()['bsclass'])) {
            $class = $object->view_index_label()['bsclass'];
        } else {
            // Rotate Color through $colorArray every $rotate_after entries
            if ($i++ % $rotate_after == 0) {
                $colorArray = \App\extensions\php\ArrayHelper::array_rotate($colorArray);
                $class = $colorArray[0];
            }
        }

        return $class;
    }

    /**
     * Evaluate according to given limits
     *
     * @param val: the value to be evaluated
     * @param limits: array of size 2 or 4, containing the limits
     * @param invert the results (good <--> bad)
     *
     * @return: evaluation results - good(0), average(1) or bad(2)
     *
     * @author: Ole Ernst
     */
    private static function colorize($val, $limit, bool $withCritical = true, bool $inv = false)
    {
        if ($withCritical && $val == 0) {
            return -1;
        }

        if ($val < $limit[0] || (isset($limit[3]) && $val > $limit[3])) {
            return $inv ? 0 : 2;
        }

        if ($val >= $limit[1]) {
            if (! isset($limit[2])) {
                return $inv ? 2 : 0;
            }

            if ($val <= $limit[2]) {
                return $inv ? 2 : 0;
            }
        }

        return 1;
    }

    /**
     * Evaluate the value in the given context
     *
     * @param dir: ds (downstream), us (upstream)
     * @param entity: the entity to check (power, modulation, ureflections, etc.)
     * @param value: array containing all values (can be used for several us/ds channels)
     * @param toString: return human readable interpretation of color value (bootstrap class)
     *
     * @return: evaluation result
     *
     * @author: Ole Ernst
     */
    public static function getQualityColor($dir, $mod, $entity, $val, $toString)
    {
        $ret = 3;
        $colors = [
            -1 => 'danger',
            0 => 'success',
            1 => 'warning',
            2 => 'bg-orange-lighter',
            3 => '',
        ];

        if (! Module::collections()->has('ProvBase')) {
            return $toString ? $colors[$ret] : $ret;
        }

        if ($val === 'n/a') {
            return $toString ? $colors[$ret] : $ret;
        }

        switch ($entity) {
            case 'Rx Power dBmV':
                $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisRxPwr'), false);
                break;
            case 'pwr':
            case 'Power dBmV':
                if ($dir == 'ds') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisDsPwr'), false);
                }
                if ($dir == 'us') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisUsPwr'));
                }
                break;
            case 'Microreflection -dBc':
                if ($val == 0) {
                    break;
                }
                $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisMicRef'));
                break;
            case 'Avg Utilization %':
                $ret = self::colorize($val, config('provbase.qualityColorThresholds.AvgUtilization'), false);
                break;
            case 'snr':
            case 'SNR dB':
            case 'MER dB':
                if ($mod == 'QPSK') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisSnrQpsk'));
                }
                if ($mod == 'QAM16') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisSnrQam16'));
                }
                if ($mod == 'QAM32') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisSnrQam32'));
                }
                // $mod == '0': no docsIfCmtsModulationTable entry
                // $dir == 'us': snr_us modem property in CustomerTopoController
                if ($mod == 'QAM64' || $mod == '0' || $dir == 'us') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisSnrQam64'));
                }
                // $dir = 'ds': snr_ds modem property in CustomerTopoController
                if ($mod == 'QAM256' || $dir == 'ds') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisSnrQam256'));
                }
                break;
                // ds_us modem property in CustomerTopoController
            case 'us':
                if ($dir == 'ds') {
                    $ret = self::colorize($val, config('provbase.qualityColorThresholds.DocsisDsUs'), false);
                }
                break;
        }

        return $toString ? $colors[$ret] : $ret;
    }

    /**
     * @param  string  $type  the type of the value
     * @param  int|float  $value  The (measured) value to be used for coloring
     * @param  array  $ruleset  Contains the thresholds for success, warning and danger
     *
     * @author Patrick Reichel
     */
    public static function getQualityColorTR069($type, $value, $monitoringConfig, $toString)
    {
        $colors = [
            0 => 'success',
            1 => 'warning',
            2 => 'danger',
            3 => '',
        ];
        $ret = 3;

        if (is_null($value)) {
            return $toString ? $colors[$ret] : $ret;
        }

        // extract all threshold setting for the given $entryType (e.g. us_snr)
        $thresholds = [
            0 => [],    // success
            1 => [],    // warning
            2 => [],    // danger
        ];
        foreach (($monitoringConfig ?? []) as $monitoringConfigGroup) {
            foreach ($monitoringConfigGroup as $monitoringConfigEntry) {
                // check if checkbox has been checked
                if (! ($monitoringConfigEntry[3][0] ?? false)) {
                    continue;
                }
                $entryType = $monitoringConfigEntry[3][4] ?? '';
                if ($type == $entryType) {
                    $thresholds[2][] = $monitoringConfigEntry[3][1] ?? null;
                    $thresholds[1][] = $monitoringConfigEntry[3][2] ?? null;
                    $thresholds[0][] = $monitoringConfigEntry[3][3] ?? null;
                }
            }
        }

        // check first for danger, then for warning and last for success state
        // most critical color wins
        for ($i = 2; $i--; $i <= 0) {
            foreach ($thresholds[$i] as $threshold) {
                if (valueInThresholdString($value, $threshold)) {
                    return $toString ? $colors[$i] : $i;
                }
            }
        }

        return $toString ? $colors[$ret] : $ret;
    }
}
