<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_fields
 */

/**
 * Hook class.
 */
class Hook_fields_date
{
    // ==============
    // Module: search
    // ==============

    /**
     * Get special Tempcode for inputting this field.
     *
     * @param  array $field The field details
     * @return ?array Specially encoded input detail rows (null: nothing special)
     */
    public function get_search_inputter(array $field) : ?array
    {
        $type = '_JUST_DATE';
        $special = $this->get_search_filter_from_env($field);
        $extra = '';
        $display = array_key_exists('trans_name', $field) ? $field['trans_name'] : get_translated_text($field['cf_name']);

        $range_search = (option_value_from_field_array($field, 'range_search', 'off') == 'on');
        if ($range_search) {
            $type .= '_RANGE';
        }

        return ['NAME' => strval($field['id']) . $extra, 'DISPLAY' => $display, 'TYPE' => $type, 'SPECIAL' => $special];
    }

    /**
     * Get special SQL from POSTed parameters for this field.
     *
     * @param  array $field The field details
     * @param  integer $i We're processing for the ith row
     * @param  string $table_alias Table alias for catalogue entry table
     * @return ?array Tuple of SQL details (array: extra trans fields to search, array: extra plain fields to search, string: an extra table segment for a join, string: the name of the field to use as a title, if this is the title, extra WHERE clause stuff) (null: nothing special)
     */
    public function inputted_to_sql_for_search(array $field, int $i, string $table_alias = 'r') : ?array
    {
        $range_search = (option_value_from_field_array($field, 'range_search', 'off') == 'on');
        if ($range_search) {
            return null;
        }

        return exact_match_sql($field, $i, 'short', $this->get_search_filter_from_env($field), $table_alias);
    }

    /**
     * Get a search filter string from the environment.
     *
     * @param  array $field The field details
     * @return string Filter
     */
    public function get_search_filter_from_env(array $field) : string
    {
        $range_search = (option_value_from_field_array($field, 'range_search', 'off') == 'on');
        if ($range_search) {
            $_from = post_param_date('option_' . strval($field['id']) . '_from', true, false);
            $from = '';
            if ($_from !== null) {
                $from = date('Y-m-d', $_from);
            }

            $_to = post_param_date('option_' . strval($field['id']) . '_to', true, false);
            $to = '';
            if ($_to !== null) {
                $to = date('Y-m-d', $_to);
            }

            return $from . ';' . $to;
        }

        $filter = post_param_date('option_' . strval($field['id']), true);
        return ($filter === null) ? '' : date('Y-m-d', $filter);
    }

    // ===================
    // Backend: fields API
    // ===================

    /**
     * Get some info bits relating to our field type, that helps us look it up / set defaults.
     *
     * @param  ?array $field The field details (null: new field)
     * @param  ?boolean $required Whether a default value cannot be blank (null: don't "lock in" a new default value) (may be passed as false also if we want to avoid "lock in" of a new default value, but in this case possible cleanup of $default may still happen where appropriate)
     * @param  ?string $default The given default value as a string (null: don't "lock in" a new default value) (blank: only "lock in" a new default value if $required is true)
     * @return array Tuple of details (row-type,default-value-to-use,db row-type)
     */
    public function get_field_value_row_bits(?array $field, ?bool $required = null, ?string $default = null) : array
    {
        if ($required !== null) {
            if (($required) && ($default == '')) {
                $default = date('Y-m-d', utctime_to_usertime());
            }
        }
        return ['short_unescaped', $default, 'short'];
    }

    /**
     * Convert a field value to something renderable.
     *
     * @param  array $field The field details
     * @param  mixed $ev The raw value
     * @param  integer $i Position in fieldset
     * @param  ?array $only_fields List of fields the output is being limited to (null: N/A)
     * @param  ?ID_TEXT $table The table we store in (null: N/A)
     * @param  ?AUTO_LINK $id The ID of the row in the table (null: N/A)
     * @param  ?ID_TEXT $id_field Name of the ID field in the table (null: N/A)
     * @param  ?ID_TEXT $field_id_field Name of the field ID field in the table (null: N/A)
     * @param  ?ID_TEXT $url_field Name of the URL field in the table (null: N/A)
     * @param  ?MEMBER $submitter Submitter (null: current member)
     * @param  ?mixed $ev_pure The 'pure' form of the raw value, meaning Comcode is not pre-parsed and string conversion has not been performed (null: unknown)
     * @return mixed Rendered field (string or Tempcode)
     */
    public function render_field_value(array &$field, $ev, int $i, ?array $only_fields, ?string $table = null, ?int $id = null, ?string $id_field = null, ?string $field_id_field = null, ?string $url_field = null, ?int $submitter = null, $ev_pure = null)
    {
        if (is_object($ev)) {
            return $ev;
        }

        if ($ev != '') {
            if (stripos($ev, 'now') !== false) {
                $time = time();
            } else {
                // Y-m-d H:i:s
                $date_bits = explode((strpos($ev, '-') !== false) ? '-' : '/', $ev, 3);
                if (!array_key_exists(1, $date_bits)) {
                    $date_bits[1] = date('m');
                }
                if (!array_key_exists(2, $date_bits)) {
                    $date_bits[2] = date('Y');
                }
                $time = cms_mktime(0, 0, 0, intval($date_bits[1]), intval($date_bits[2]), @intval($date_bits[0]));
            }
            $ev = get_timezoned_date($time, false, true);
        }
        return escape_html($ev);
    }

    // ======================
    // Frontend: fields input
    // ======================

    /**
     * Find a year range limiter part based on field options.
     *
     * @param  array $field The field details
     * @param  string $type The limit type
     * @set min_year max_year
     * @return integer Year range part
     */
    protected function find_year_range_limiter(array $field, string $type) : int
    {
        $fo = option_value_from_field_array($field, $type, '');

        $matches = [];
        if (preg_match('#^Y([\-+]\d+)$#i', $fo, $matches) != 0) {
            return intval(date('Y')) + intval($matches[1]);
        }

        if ($fo == '') {
            switch ($type) {
                case 'min_year':
                    return 0;

                case 'max_year':
                    return 3000;
            }
        }

        return intval($fo);
    }

    /**
     * Get form inputter.
     *
     * @param  string $_cf_name The field name
     * @param  string $_cf_description The field description
     * @param  array $field The field details
     * @param  ?string $actual_value The actual current value of the field (null: none)
     * @param  boolean $new Whether this is for a new entry
     * @return ?Tempcode The Tempcode for the input field (null: skip the field - it's not input)
     */
    public function get_field_inputter(string $_cf_name, string $_cf_description, array $field, ?string $actual_value, bool $new) : ?object
    {
        $time = mixed();

        if (empty($actual_value)) {
            $time = null;
        } elseif (stripos($actual_value, 'now') !== false) {
            $time = time();
        } else {
            // Y-m-d
            $date_bits = explode((strpos($actual_value, '-') !== false) ? '-' : '/', $actual_value, 3);
            if (!array_key_exists(1, $date_bits)) {
                $date_bits[1] = date('m');
            }
            if (!array_key_exists(2, $date_bits)) {
                $date_bits[2] = date('Y');
            }

            $time = [0, 0, intval($date_bits[1]), intval($date_bits[2]), intval($date_bits[0])];
        }

        $min_year = $this->find_year_range_limiter($field, 'min_year');
        $max_year = $this->find_year_range_limiter($field, 'max_year');
        $years_to_show = $max_year - $min_year;

        $input_name = @cms_empty_safe($field['cf_input_name']) ? ('field_' . strval($field['id'])) : $field['cf_input_name'];
        $autocomplete = ($new && !empty($field['cf_autofill_type'])) ? (($field['cf_autofill_hint'] ? ($field['cf_autofill_hint'] . ' ') : '') . $field['cf_autofill_type']) : null;

        return form_input_date($_cf_name, $_cf_description, $input_name, $field['cf_required'] == 1, ($field['cf_required'] == 0) && ($actual_value == ''), false, $time, $years_to_show, $min_year, null, true, null, false, $autocomplete);
    }

    /**
     * Find the posted value from the get_field_inputter field.
     *
     * @param  boolean $editing Whether we were editing (because on edit, it could be a fractional edit)
     * @param  array $field The field details
     * @param  ?string $upload_dir Where the files will be uploaded to (null: do not store an upload, return null if we would need to do so)
     * @param  ?array $old_value Former value of field (null: none)
     * @return ?string The value (null: could not process)
     */
    public function inputted_to_field_value(bool $editing, array $field, ?string $upload_dir = 'uploads/catalogues', ?array $old_value = null) : ?string
    {
        $id = $field['id'];
        $stub = 'field_' . strval($id);

        require_code('temporal2');
        list($year, $month, $day) = post_param_date_components($stub);
        if ($year === null) {
            return $editing ? STRING_MAGIC_NULL : '';
        }
        if ($month === null) {
            return $editing ? STRING_MAGIC_NULL : '';
        }
        if ($day === null) {
            return $editing ? STRING_MAGIC_NULL : '';
        }

        $min_year = $this->find_year_range_limiter($field, 'min_year');
        $max_year = $this->find_year_range_limiter($field, 'max_year');
        if ((($min_year !== null) && ($year < $min_year)) || (($max_year !== null) && ($year > $max_year))) {
            warn_exit(do_lang_tempcode('INVALID_DATE_GIVEN'));
        }

        return str_pad(strval($year), 4, '0', STR_PAD_LEFT) . '-' . str_pad(strval($month), 2, '0', STR_PAD_LEFT) . '-' . str_pad(strval($day), 2, '0', STR_PAD_LEFT);
    }

    /**
     * Determine what data should be used from this field in SEO.
     *
     * @param  string $val The value of the field
     * @param  integer $field_id The ID of the field
     * @param  ID_TEXT $content_type The content type using this field
     * @param  ?ID_TEXT $content_id The ID of the content using this field (null: not using a specific piece of content, such as adding a new entry)
     * @return mixed Either a string of the content to use in SEO, or a Tuple of [(string) content to use, (boolean) must use / high priority, (boolean) is a codename]
     */
    public function get_seo_source_map(string $val, int $field_id, string $content_type, ?string $content_id = null)
    {
        return ''; // Dates and times have no meaning in SEO without context
    }

    /**
     * Define what type of field this should be treated as in the privacy system if marked sensitive.
     * This method should be defined on fields which should not be treated as "additional_anonymise_fields".
     *
     * @param  array $field The field details
     * @return ID_TEXT The type of field to treat this
     */
    public function privacy_field_type(array $field) : string
    {
        return 'number_field_anonymise_only';
    }
}
