<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This file contains the surveyprofield_numeric
 *
 * @package   surveyprofield_numeric
 * @copyright 2013 onwards kordan <stringapiccola@gmail.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace surveyprofield_numeric;

defined('MOODLE_INTERNAL') || die();

use core_text;
use mod_surveypro\itembase;

require_once($CFG->dirroot . '/mod/surveypro/field/numeric/lib.php');

/**
 * Class to manage each aspect of the numeric item
 *
 * @package   surveyprofield_numeric
 * @copyright 2013 onwards kordan <stringapiccola@gmail.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class item extends itembase
{
    // Itembase properties.

    /**
     * @var int Defaultvalue for the item answer
     */
    protected $defaultvalue;

    /**
     * @var bool Is the number signed?
     */
    protected $signed;

    /**
     * @var int Number lowerbound
     */
    protected $lowerbound;

    /**
     * @var int Number upperbound
     */
    protected $upperbound;

    /**
     * @var int Number of decimals allowed for this number
     */
    protected $decimals;

    // Service variables.

    /**
     * @var string Decimal separator
     */
    protected $decimalseparator;

    // Service variables.

    /**
     * @var bool Does this item use the child table surveypro(field|format)_plugin?
     */
    protected static $usesplugintable = true;

    /**
     * @var bool Can this item be parent?
     */
    protected static $canbeparent = false;

    /**
     * Class constructor.
     *
     * If itemid is provided, load the object (item + base + plugin) from database
     * If evaluateparentcontent is true, load the parentitem parentcontent property too
     *
     * @param \stdClass $cm
     * @param object $surveypro
     * @param int $itemid Optional item ID
     * @param bool $getparentcontent True to include $item->parentcontent (as decoded by the parent item) too, false otherwise
     */
    public function __construct($cm, $surveypro, $itemid, $getparentcontent) {
        parent::__construct($cm, $surveypro, $itemid, $getparentcontent);

        // List of properties set to static values.
        $this->type = SURVEYPRO_TYPEFIELD;
        $this->plugin = 'numeric';

        // Override the list of fields using format, whether needed.
        // Nothing to override, here.

        // Other element specific properties.
        $this->decimalseparator = get_string('decsep', 'langconfig');

        // Override properties depending from $surveypro settings.
        // No properties here.

        // List of fields of the base form I do not want to have in the item definition.
        // Each (field|format) plugin receive a list of fields (quite) common to each (field|format) plugin.
        // This is the list of the elements of the itembase form fields that this (field|format) plugin does not use.
        // Empty list.

        if (!empty($itemid)) {
            $this->item_load($itemid, $getparentcontent);
        }
    }

    /**
     * Item load.
     *
     * @param int $itemid
     * @param bool $getparentcontent True to include $item->parentcontent (as decoded by the parent item) too, false otherwise
     * @return void
     */
    public function item_load($itemid, $getparentcontent) {
        parent::item_load($itemid, $getparentcontent);

        // Multilang load support for builtin surveypro.
        // Whether executed, the 'content' field is ALWAYS handled.
        $this->item_builtin_string_load_support();

        $this->item_custom_fields_to_form();
    }

    /**
     * Item save.
     *
     * @param object $record
     * @return void
     */
    public function item_save($record) {
        // Set properties at plugin level and then continue to base level.

        // Set custom fields values as defined by this specific plugin.
        $this->add_plugin_properties_to_record($record);

        // Do parent item saving stuff here (mod_surveypro_itembase::item_save($record))).
        return parent::item_save($record);
    }

    /**
     * Item add mandatory plugin fields
     * Copy mandatory fields to $record
     *
     * @param \stdClass $record
     * @return void
     */
    public function item_add_fields_default_to_child_table(&$record) {
        // $record->defaultvalue
        $record->signed = 0;
        // $record->lowerbound
        // $record->upperbound
        $record->decimals = 0;
    }

    /**
     * Prepare values for the mform of this item.
     *
     * @return void
     */
    public function item_custom_fields_to_form() {
        // 1. Special management for composite fields.
        // Nothing to do: they don't exist in this plugin.

        // 2. float numbers need more attention because I can write them using , or .
        // Do not chenage lowerbound and upperbound as they will be used in comparisons
        // so I need them with the american numeric format.
        // I will change them at printing time.
        // $numericfields = ['defaultvalue', 'lowerbound', 'upperbound'];
        $numericfields = ['defaultvalue'];
        foreach ($numericfields as $numericfield) {
            if (core_text::strlen($this->{$numericfield})) {
                $this->{$numericfield} = format_float($this->{$numericfield}, $this->decimals);
            }
        }
    }

    /**
     * Traslate values from the mform of this item to values for corresponding properties.
     *
     * @param object $record
     * @return void
     */
    public function add_plugin_properties_to_record($record) {
        // 1. Special management for composite fields.
        // Nothing to do: they don't exist in this plugin.

        // 2. Override few values.
        // Nothing to do: no need to overwrite variables.

        // 3. Set values corresponding to checkboxes.
        // Take care: 'required', 'hideinstructions' were already considered in get_common_settings.
        // Nothing to do: no checkboxes in this plugin item form.

        // 4. Other: float numbers need more attention because I can write them using , or .
        $numericfields = ['defaultvalue', 'lowerbound', 'upperbound'];
        foreach ($numericfields as $numericfield) {
            if (core_text::strlen($record->{$numericfield})) {
                $record->{$numericfield} = $this->get_international_number($record->{$numericfield});
            } else {
                $record->{$numericfield} = null;
            }
        }
    }

    // MARK set.

    /**
     * Set defaultvalue.
     *
     * @param string $defaultvalue
     * @return void
     */
    public function set_defaultvalue($defaultvalue) {
        $this->defaultvalue = $defaultvalue;
    }

    /**
     * Set signed.
     *
     * @param string $signed
     * @return void
     */
    public function set_signed($signed) {
        $this->signed = $signed;
    }

    /**
     * Set lowerbound.
     *
     * @param string $lowerbound
     * @return void
     */
    public function set_lowerbound($lowerbound) {
        $this->lowerbound = $lowerbound;
    }

    /**
     * Set upperbound.
     *
     * @param string $upperbound
     * @return void
     */
    public function set_upperbound($upperbound) {
        $this->upperbound = $upperbound;
    }

    /**
     * Set decimals.
     *
     * @param string $decimals
     * @return void
     */
    public function set_decimals($decimals) {
        $this->decimals = $decimals;
    }

    /**
     * Set decimalseparator.
     *
     * @param string $decimalseparator
     * @return void
     */
    public function set_decimalseparator($decimalseparator) {
        $this->decimalseparator = $decimalseparator;
    }

    // MARK get.

    /**
     * Get defaultvalue.
     *
     * @return $this->defaultvalue
     */
    public function get_defaultvalue() {
        return $this->defaultvalue;
    }

    /**
     * Get signed.
     *
     * @return $this->signed
     */
    public function get_signed() {
        return $this->signed;
    }

    /**
     * Get lowerbound.
     *
     * @return $this->lowerbound
     */
    public function get_lowerbound() {
        return $this->lowerbound;
    }

    /**
     * Get upperbound.
     *
     * @return $this->upperbound
     */
    public function get_upperbound() {
        return $this->upperbound;
    }

    /**
     * Get decimals.
     *
     * @return $this->decimals
     */
    public function get_decimals() {
        return $this->decimals;
    }

    /**
     * Get decimalseparator.
     *
     * @return $this->decimalseparator
     */
    public function get_decimalseparator() {
        return $this->decimalseparator;
    }

    /**
     * Get the requested property.
     *
     * @param string $field
     * @return the content of the field or false if it is not set.
     */
    public function get_generic_property($field) {
        if (isset($this->{$field})) {
            $condition = false;
            $condition = $condition || ($field == 'lowerbound');
            $condition = $condition || ($field == 'upperbound');
            $condition = $condition || ($field == 'defaultvalue');
            if ($condition) {
                $return = $this->get_international_number($this->{$field});
            } else {
                $method = 'get_' . $field;
                $return = $this->{$method}();
            }
        } else {
            $return = false;
        }

        return $return;
    }

    /**
     * get_international_number
     * starting from localnumber written using local decimal separator,
     * this function returns the same number written using the dot as decimal separator.
     *
     * @param double|bool $localnumber false in case of error
     * @return void or double $internationalnumber
     */
    public function get_international_number($localnumber) {
        $localnumber = trim($localnumber);
        $internationalnumber = str_replace($this->decimalseparator, '.', $localnumber);
        if (is_numeric($internationalnumber)) {
            $return = $internationalnumber;
        } else {
            $return = false;
        }

        return $return;
    }

    /**
     * Prepare presets for itemsetuprform with the help of the parent class too.
     *
     * @return array $data
     */
    public function get_plugin_presets() {
        $pluginproperties = ['defaultvalue', 'signed', 'lowerbound', 'upperbound', 'decimals'];
        $data = $this->get_base_presets($pluginproperties);

        return $data;
    }

    /**
     * Make the list of the fields using multilang
     *
     * @param bool $includemetafields
     * @return array of fields
     */
    public function get_multilang_fields($includemetafields = true) {
        $fieldlist['surveypro_item'] = $this->get_base_multilang_fields($includemetafields);
        $fieldlist['surveyprofield_numeric'] = [];

        return $fieldlist;
    }

    /**
     * Return the xml schema for surveypro_<<plugin>> table.
     *
     * @return string $schema
     */
    public static function get_plugin_schema() {
        $schema = <<<EOS
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
    <xs:element name="surveyprofield_numeric">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="defaultvalue" type="xs:decimal" minOccurs="0"/>
                <xs:element name="signed" type="xs:int" minOccurs="0"/>
                <xs:element name="lowerbound" type="xs:decimal" minOccurs="0"/>
                <xs:element name="upperbound" type="xs:decimal" minOccurs="0"/>
                <xs:element name="decimals" type="xs:int" minOccurs="0"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>
EOS;

        return $schema;
    }

    // MARK userform.

    /**
     * Define the mform element for the userform and the searchform.
     *
     * @param \moodleform $mform
     * @param int $searchformelementscount // 0 means: I am not drawing this element in a search form.
     * @param bool $readonly
     * @return void
     */
    public function userform_mform_element($mform, $searchformelementscount, $readonly) {
        $starstr = get_string('star', 'mod_surveypro');
        if ($this->position == SURVEYPRO_POSITIONLEFT) {
            $elementlabel = $this->get_contentwithnumber();
        } else {
            $elementlabel = '&nbsp;';
        }

        $attributes = [];
        $elementgroup = [];
        $class = ['class' => 'indent-' . $this->indent];
        $baseid = 'id_field_numeric_' . $this->sortindex;
        $basename = $this->itemname;

        $attributes['id'] = $baseid;
        // Cool for browsers supporting html 5.
        // $attributes['type'] = 'number';
        // But it doesn't work because "type" property is reserved to mform library.

        if (!$searchformelementscount) {
            $elementgroup[] = $mform->createElement('text', $basename, $elementlabel, $attributes);
            $mform->addGroup($elementgroup, $basename . '_group', $elementlabel, ' ', false, $class);
            $mform->setType($basename, PARAM_RAW); // See: moodlelib.php lines 133+.

            if ($this->required) {
                // Even if the item is required I CAN NOT ADD ANY RULE HERE because...
                // I do not want JS form validation if the page is submitted through the "previous" button.
                // I do not want JS field validation even if this item is required BUT disabled. See: MDL-34815.
                // Because of this, I simply add a dummy star to the item and the footer note about mandatory fields.
                $starplace = ($this->position == SURVEYPRO_POSITIONTOP) ? $basename . '_extrarow_group' : $basename . '_group';
                $mform->_required[] = $starplace;
            }
        } else {
            $elementgroup[] = $mform->createElement('text', $basename, '', $attributes);
            $mform->setType($basename, PARAM_RAW);

            if ($searchformelementscount > 1) {
                $attributes['id'] = $baseid . '_ignoreme';
                $elementgroup[] = $mform->createElement('checkbox', $basename . '_ignoreme', '', $starstr, $attributes);
            }

            $mform->addGroup($elementgroup, $basename . '_group', $elementlabel, ' ', false, $class);
            if ($searchformelementscount > 1) {
                $mform->disabledIf($basename . '_group', $basename . '_ignoreme', 'checked');
            }
        }

        // Begin of: defaults.
        if (!$searchformelementscount) {
            if (!$readonly) {
                if (core_text::strlen($this->defaultvalue)) {
                    $mform->setDefault($basename, "$this->defaultvalue");
                }
            }
        } else {
            if ($searchformelementscount > 1) {
                $mform->setDefault($basename . '_ignoreme', '1');
            }
        }
        // End of: defaults.
    }

    /**
     * Perform userform and searchform data validation.
     *
     * @param array $data
     * @param array $errors
     * @param bool $searchform
     * @return void
     */
    public function userform_mform_validation($data, &$errors, $searchform) {
        if ($searchform) {
            return $errors;
        }

        $errorkey = $this->itemname . '_group';

        $draftuserinput = $data[$this->itemname];
        if (!core_text::strlen($draftuserinput)) {
            if (!empty($this->required)) {
                $errors[$errorkey] = get_string('required');
            }
            return $errors;
        }

        $userinput = $this->get_international_number($draftuserinput);

        if (!is_numeric($userinput)) {
            // It is not a number, shouts.
            $errors[$errorkey] = get_string('uerr_notanumber', 'surveyprofield_numeric');
            return $errors;
        } else {
            // If it is < 0 but has been defined as unsigned, shouts.
            if (!$this->signed && ($userinput < 0)) {
                $errors[$errorkey] = get_string('uerr_negative', 'surveyprofield_numeric');
            }
            // If it has decimal but has been defined as integer, shouts.
            $isinteger = (bool)(strval(intval($userinput)) == strval($userinput));
            if (($this->decimals == 0) && (!$isinteger)) {
                $errors[$errorkey] = get_string('uerr_notinteger', 'surveyprofield_numeric');
            }
        }

        $haslowerbound = (core_text::strlen($this->lowerbound));
        $hasupperbound = (core_text::strlen($this->upperbound));

        if ($haslowerbound && $hasupperbound) {
            // Internal range.
            if (($userinput < $this->lowerbound) || ($userinput > $this->upperbound)) {
                $errors[$errorkey] = get_string('uerr_outofinternalrange', 'surveyprofield_numeric');
            }

            if ($this->lowerbound > $this->upperbound) {
                // External range.
                if (($userinput > $this->lowerbound) && ($userinput < $this->upperbound)) {
                    $a = new \stdClass();
                    $a->lowerbound = format_float($this->lowerbound, $this->decimals);
                    $a->upperbound = format_float($this->upperbound, $this->decimals);
                    $errors[$errorkey] = get_string('uerr_outofexternalrange', 'surveyprofield_numeric', $a);
                }
            }
        } else {
            if ($haslowerbound && ($userinput < $this->lowerbound)) {
                $errors[$errorkey] = get_string('uerr_lowerthanminimum', 'surveyprofield_numeric');
            }
            if ($hasupperbound && ($userinput > $this->upperbound)) {
                $errors[$errorkey] = get_string('uerr_greaterthanmaximum', 'surveyprofield_numeric');
            }
        }

        return $errors;
    }

    /**
     * Prepare the string with the filling instruction.
     *
     * @return string $fillinginstruction
     */
    public function userform_get_filling_instructions() {

        $haslowerbound = (core_text::strlen($this->lowerbound));
        $hasupperbound = (core_text::strlen($this->upperbound));
        $arrayinstruction = [];

        if (!empty($this->signed)) {
            $arrayinstruction[] = get_string('restriction_hassign', 'surveyprofield_numeric');
        }

        if ($haslowerbound && $hasupperbound) {
            $a = new \stdClass();
            $a->lowerbound = format_float($this->lowerbound, $this->decimals);
            $a->upperbound = format_float($this->upperbound, $this->decimals);

            $arrayinstruction[] = get_string('restriction_lowerupper', 'surveyprofield_numeric', $a);
        } else {
            if ($haslowerbound) {
                $a = format_float($this->lowerbound, $this->decimals);
                $arrayinstruction[] = get_string('restriction_lower', 'surveyprofield_numeric', $a);
            }

            if ($hasupperbound) {
                $a = format_float($this->upperbound, $this->decimals);
                $arrayinstruction[] = get_string('restriction_upper', 'surveyprofield_numeric', $a);
            }
        }

        if (!empty($this->decimals)) {
            $a = $this->decimals;
            $arrayinstruction[] = get_string('restriction_hasdecimals', 'surveyprofield_numeric', $a);
            $arrayinstruction[] = get_string('decimalautofix', 'surveyprofield_numeric');
            // This sentence dials about decimal separator not about the expected value.
            // So I leave it as last sentence.
            $arrayinstruction[] = get_string('declaredecimalseparator', 'surveyprofield_numeric', $this->decimalseparator);
        } else {
            $arrayinstruction[] = get_string('restriction_isinteger', 'surveyprofield_numeric');
        }

        if (count($arrayinstruction)) {
            $fillinginstruction = implode('; ', $arrayinstruction);
        } else {
            $fillinginstruction = '';
        }

        return $fillinginstruction;
    }

    /**
     * Starting from the info set by the user in the form
     * this method calculates what to save in the db
     * or what to return for the search form.
     * I don't set $olduseranswer->contentformat in order to accept the default db value.
     *
     * @param array $answer
     * @param object $olduseranswer
     * @param bool $searchform
     * @return void
     */
    public function userform_prepare_user_answer($answer, &$olduseranswer, $searchform) {
        if (isset($answer['ignoreme'])) {
            $olduseranswer->content = null;
            return;
        }

        $content = trim($answer['mainelement']);
        if (!core_text::strlen($content)) {
            $olduseranswer->content = '';
        } else {
            $content = $this->get_international_number($content);
            $olduseranswer->content = round($content, $this->decimals);
        }
    }

    /**
     * This method is called from get_prefill_data (in formbase.class.php) to set $prefill at user form display time.
     *
     * @param object $fromdb
     * @return associative array with disaggregate element values
     */
    public function userform_get_prefill($fromdb) {
        $prefill = [];

        if (!$fromdb) { // Param $fromdb may be boolean false for not existing data.
            return $prefill;
        }

        // This number comes from the db so it can ONLY have '.' as decimal separator.
        if (isset($fromdb->content) && !empty($fromdb->content)) {
            $prefill[$this->itemname] = number_format((double)$fromdb->content, $this->decimals, $this->decimalseparator, '');
        }

        return $prefill;
    }

    /**
     * Returns an array with the names of the mform element added using $mform->addElement or $mform->addGroup.
     *
     * @return array
     */
    public function userform_get_root_elements_name() {
        return [$this->itemname];
    }
}
