<?php

/**
 * Media Sync
 *
 * This class is used for generating main content and also to import files to database
 *
 * @package     MediaSync
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       0.1.0
 * @author      Erol Živina
 */
if ( !class_exists( 'MediaSync' ) ) :

    class MediaSync
    {

        /**
         * Since PHP on Windows Server recognizes both backslash ("\") or forward slash ("/"),
         * and WordPress always uses forward slashes (in it's built in functions),
         * we'll also always use forward slashes to solve various issues when using Windows Server.
         */
        const MEDIA_SYNC_DS = '/';

        /**
         * Absolute path to main "uploads" directory.
         *
         * @var null
         */
        static $upload_dir_path = null;

        /**
         * Files already in Media Library.
         *
         * @var null|array
         */
        static $files_in_db = null;

        /**
         * For showing passed time when debugging.
         *
         * @since 1.2.0
         * @var null|string
         */
        static $start_time = null;


        /**
         * Render main plugin content
         *
         * @since 0.1.0
         * @return void
         */
        static public function media_sync_main_page()
        {

            if(!MediaSync::media_sync_user_has_general_access()) {
                wp_die(__('You do not have sufficient permissions to access this page.', 'media-sync'));
            }

            $is_debug = self::media_sync_is_debug();
            if($is_debug) {
                @ini_set('display_errors', 1);

                // Enable WP_DEBUG mode
                if ( !defined('WP_DEBUG') ) {
                    define('WP_DEBUG', true);
                }

                // Enable Debug logging to the /wp-content/debug.log file
                if ( !defined('WP_DEBUG_LOG') ) {
                    define('WP_DEBUG_LOG', true);
                }

                // Enable display of errors and warnings
                if ( !defined('WP_DEBUG_DISPLAY') ) {
                    define('WP_DEBUG_DISPLAY', true);
                }

                // Use dev versions of core JS and CSS files (only needed if you are modifying these core files)
                if ( !defined('SCRIPT_DEBUG') ) {
                    define('SCRIPT_DEBUG', true);
                }

                // 512 MB
                @ini_set('memory_limit', '512M');

                // 10 min
                @ini_set('max_execution_time', '600');
            }


            $scan_files = isset($_GET['scan_files']) && $_GET['scan_files'] == true;
            $associated_filter = isset($_GET['associated-filter']) && !empty($_GET['associated-filter']) ? explode(':', urldecode($_GET['associated-filter'])) : null;
            $missing_from_ml = $associated_filter && $associated_filter[0] == 'missing_from' && $associated_filter[1] == 'media_library';

            $here = esc_url(get_admin_url(null, 'upload.php?page=media-sync-page'));

            $upload_dir_path = self::media_sync_get_uploads_basedir();
            $relative_path = self::media_sync_get_relative_path($upload_dir_path);
            $no_upload_dir_message = __('Scan directory not found. Please check settings.', 'media-sync');
            ?>

            <div class="wrap main-media-sync-page" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
                <h1><?= __('Media Sync', 'media-sync') ?></h1>

                <?php do_action('media_sync_extended_initial_notice'); ?>

                <div class="notice notice-error hidden media-sync-global-errors js-media-sync-error-placeholder"></div>

                <?php if ($scan_files) : ?>
                    <div class="notice notice-error">
                        <p><?= __('Please backup your database! This plugin makes database changes.', 'media-sync') ?></p>
                    </div>
                    <div class="notice notice-success notice-files-imported">
                        <p><?= sprintf(__('Done! Highlighted files were successfully imported. %s to see changes.', 'media-sync'),
                                '<a href="'.add_query_arg('scan_files', 1, $here).'">'.__('Re-scan', 'media-sync').'</a>') ?></p>
                    </div>
                <?php endif; ?>

                <div class="media-sync-list-files">
                    <form action="<?= $here ?>" method="GET">
                        <input type="hidden" name="page" value="media-sync-page"/>
                        <input type="hidden" name="scan_files" value="<?= $scan_files ?>"/>
                        <input type="hidden" name="is_debug" value="<?= $is_debug ?>"/>
                        <div class="media-sync-buttons-holder">
                            <?php if (!$scan_files) : ?>
                                <div class="card">
                                    <h2 class="title"><?= __('Sync - uploads directory', 'media-sync') ?></h2>

                                    <?php if($upload_dir_path) : ?>
                                        <a class="button button-primary" href="<?= add_query_arg('scan_files', 1, $here) ?>">
                                            <?= __('Scan Files', 'media-sync') ?>
                                        </a>

                                        <p class="media-sync-scan-files-message">
                                            <?= sprintf(__('Use this to see content of upload dir: %s and import files to Media Library.', 'media-sync'),
                                                '<code title="'.$upload_dir_path.'">'.$relative_path.'</code>') ?>
                                        </p>
                                    <?php else: ?>
                                        <p><i><?= $no_upload_dir_message ?></i></p>
                                    <?php endif; ?>
                                </div>
                                <div class="card">
                                    <h2 class="title"><?= __('Sync - Media Library', 'media-sync') ?></h2>
                                    <a class="button button-primary" href="<?= add_query_arg(array('mode' => 'list', 'media_sync_missing_files' => 'yes'), get_admin_url(null, 'upload.php')) ?>">
                                        <?= __('Filter Media Library', 'media-sync') ?>
                                    </a>

                                    <p class="media-sync-scan-files-message">
                                        <?= __('Use this to see Media Library items that are missing actual files. This takes you to Media Library but with custom filter.', 'media-sync') ?>
                                    </p>
                                </div>
                            <?php endif; ?>

                            <?php if ($scan_files) : ?>
                                <div class="media-sync-button-holder">
                                    <a class="button button-primary" href="<?= add_query_arg('scan_files', 1, $here) ?>">
                                        <?= __('Re-scan', 'media-sync') ?>
                                    </a>
                                </div>

                                <div class="media-sync-button-holder">
                                    <button class="button button-primary js-import-selected"><?= __('Import Selected', 'media-sync') ?></button>
                                    <span class="spinner import-spinner"></span>
                                </div>

                                <div class="import-options">
                                    <?php if ( !!get_option( 'ms_sg_use_dry_run', 1 ) ) : ?>
                                        <div class="media-sync-dry-run-holder">
                                            <input type="checkbox" id="dry-run" name="dry_run" checked="checked" />
                                            <label for="dry-run"><?= __('Dry Run (test without making database changes)', 'media-sync') ?></label>
                                        </div>
                                    <?php else: ?>
                                        <input type="hidden" name="dry_run" value="" />
                                    <?php endif; ?>

                                    <div class="import-option-date-type">
                                        <label><?= __('Date/time to set for newly imported files', 'media-sync') ?>:</label>
                                        <?php self::media_sync_render_post_date_option( 'file_post_date' ) ?>
                                    </div>

                                    <div class="import-option-batch-size">
                                        <label for="batch-size">
                                            <?= __('Batch Size:', 'media-sync') ?>
                                            <input type="number" value="10" step="1" min="1" class="small-text" name="batch_size" id="batch-size" />
                                        </label>
                                        <p class="description">
                                            <?= __('The number of files to process per network request.', 'media-sync') ?>
                                            <?= __('A lower value might avoid some limits of your server (e.g. max_execution_time). ', 'media-sync') ?>
                                            <br />
                                            <?= __('If you still get errors, please try disabling other plugins related to files/images.', 'media-sync') ?>
                                            <?= __('To reduce additional actions related to file uploads (e.g. generating additional thumbnails).', 'media-sync') ?>
                                        </p>
                                    </div>
                                </div>
                            <?php endif; ?>
                        </div>

                        <?php if ($scan_files) : ?>
                            <p class="media-sync-state-holder">
                                <?php if (!$is_debug) : ?>
                                    <span class="media-sync-progress-holder">
                                        <span class="media-sync-progress"></span>
                                    </span>
                                <?php endif; ?>
                                <span class="media-sync-state">
                                    <?php if (!$is_debug) : ?>
                                        <span class="media-sync-state-text">
                                            <?= __('Imported', 'media-sync') ?>
                                        </span>
                                        <span class="media-sync-state-number media-sync-imported-count js-media-sync-imported-count">0</span>
                                        <span class="media-sync-state-text">
                                            <?= __('out of', 'media-sync') ?>
                                        </span>
                                    <?php endif; ?>
                                    <span class="media-sync-state-number media-sync-selected-count js-media-sync-selected-count">0</span>
                                    <span class="media-sync-state-text">
                                        <?= __('selected items', 'media-sync') ?>
                                    </span>
                                </span>
                                <span class="media-sync-state media-sync-state-note">
                                    <?= __('Files already in Media Library will be skipped during import', 'media-sync') ?>
                                </span>
                            </p>

                            <div class="wp-filter">
                                <div class="filter-items">
                                    <label for="associated-filter" class="screen-reader-text"><?= __('Filter by type', 'media-sync') ?></label>
                                    <select class="associated-filters" name="associated-filter" id="associated-filter">
                                        <option value=""><?= __('All files', 'media-sync') ?></option>
                                        <option value="missing_from:media_library"<?= $missing_from_ml ? 'selected="selected"' : '' ?>>
                                            <?= __('Only files missing from Media Library', 'media-sync') ?>
                                        </option>
                                    </select>

                                    <div class="actions">
                                        <input type="submit" name="filter_action" id="post-query-submit" class="button" value="<?= __('Filter', 'media-sync') ?>">
                                    </div>
                                </div>
                            </div>

                            <?php if ($is_debug) : ?>
                                <div class="media-sync-html-response-holder">
                                    <div class="media-sync-html-response-title"><?= __('Raw import results', 'media-sync') ?>:</div>
                                    <div class="media-sync-html-responses js-media-sync-html-responses"></div>
                                </div>
                            <?php endif; ?>

                            <?php
                            $tree = $upload_dir_path ? self::media_sync_get_list_of_uploads() : array();
                            $current_dir = get_option( 'ms_sg_scan_sub_dir' ) ? $relative_path : null;
                            ?>
                            <?php if (!empty($tree)) : ?>
                                <div class="media-sync-table-holder">
                                    <table class="wp-list-table widefat fixed media">
                                        <?php self::media_sync_render_thead_tfoot_row('thead', $current_dir) ?>
                                        <tbody id="the-list">
                                        <?php foreach ($tree as $item) : ?>
                                            <?php self::media_sync_render_row($item) ?>
                                        <?php endforeach; ?>
                                        </tbody>
                                        <?php self::media_sync_render_thead_tfoot_row('tfoot') ?>
                                    </table>
                                    <span class="spinner is-active table-spinner"></span>
                                </div>

                                <?php
                                self::media_sync_render_debug_stats('after table render');
                                ?>
                            <?php else : ?>
                                <p class="media-sync-no-results">
                                    <?php if ($missing_from_ml) : ?>
                                        <?= __('Everything seems fine here, there are no files that are not already in your Media Library.', 'media-sync') ?>
                                    <?php elseif(!$upload_dir_path) : ?>
                                        <?= $no_upload_dir_message ?>
                                    <?php else : ?>
                                        <?= __('No Results', 'media-sync') ?>
                                    <?php endif; ?>
                                </p>
                            <?php endif; ?>
                        <?php endif; ?>
                    </form>
                </div>
            </div>

            <?php
        }


        /**
         * Prepare plugin settings
         *
         * @since 1.1.0
         * @return void|boolean
         */
        static public function media_sync_options_setup()
        {
            if ( !current_user_can( 'manage_options' ) ) {
                return false;
            }

            // From this plugin
            add_option( 'ms_sg_scan_sub_dir', '' );
            add_option( 'ms_sg_use_dry_run', 1 );
            add_option( 'ms_sg_file_post_date', 'default' );
            add_option( 'ms_sg_use_debug', 0 );
            register_setting( 'media-sync-settings-group', 'ms_sg_scan_sub_dir' );
            register_setting( 'media-sync-settings-group', 'ms_sg_use_dry_run' );
            register_setting( 'media-sync-settings-group', 'ms_sg_file_post_date' );
            register_setting( 'media-sync-settings-group', 'ms_sg_use_debug' );

            // From Add-On
            do_action('media_sync_extended_advanced_options_register');
        }


        /**
         * Render plugin settings
         *
         * @since 1.1.0
         * @return void|boolean
         */
        static public function media_sync_options_page()
        {
            if ( !current_user_can( 'manage_options' ) ) {
                return false;
            }
            ?>
            <div class="wrap media-sync-page-settings">
                <h1><?= __('Media Sync Settings', 'media-sync') ?></h1>

                <form method="post" action="options.php">
                    <?php
                    // From this plugin
                    settings_fields( 'media-sync-settings-group' );
                    do_settings_sections( 'media-sync-settings-group' );
                    ?>

                    <table class="form-table">
                        <tbody>
                            <?php
                            // From this plugin
                            ?>
                            <tr>
                                <th scope="row">
                                    <label for="ms-sg-scan-sub-dir"><?= __('Scan directory', 'media-sync') ?></label>
                                </th>
                                <td>
                                    <fieldset>
                                        <?php
                                        $upload_dir_path = self::media_sync_get_uploads_basedir(false);
                                        // Since this path is always using forward slashes, we're also adding forward slash ("/")
                                        $relative_path = self::media_sync_get_relative_path($upload_dir_path) . self::MEDIA_SYNC_DS;
                                        ?>
                                        <span><?= $relative_path ?></span>
                                        <input type="text" value="<?= get_option( 'ms_sg_scan_sub_dir' ) ?>" name="ms_sg_scan_sub_dir" id="ms-sg-scan-sub-dir" />
                                    </fieldset>
                                </td>
                            </tr>
                            <tr>
                                <th scope="row">
                                    <label><?= __('Dry Run', 'media-sync') ?></label>
                                </th>
                                <td>
                                    <fieldset>
                                        <label for="ms-sg-use-dry-run">
                                            <?php $checked = checked( 1, get_option( 'ms_sg_use_dry_run' ), false ); ?>
                                            <input type="checkbox" name="ms_sg_use_dry_run" id="ms-sg-use-dry-run" value="1" <?= $checked ?>>
                                            <?= __('Show "Dry Run" option', 'media-sync') ?>
                                        </label>
                                    </fieldset>
                                </td>
                            </tr>
                            <tr>
                                <th scope="row">
                                    <label><?= __('Preselected / default value for file date', 'media-sync') ?></label>
                                </th>
                                <td>
                                    <?php self::media_sync_render_post_date_option( 'ms_sg_file_post_date' ) ?>
                                </td>
                            </tr>
                            <tr>
                                <th scope="row">
                                    <label><?= __('Turn on debugging', 'media-sync') ?></label>
                                </th>
                                <td>
                                    <fieldset>
                                        <label for="ms-sg-use-debugging">
                                            <?php $checked = checked( 1, get_option( 'ms_sg_use_debug' ), false ); ?>
                                            <input type="checkbox" name="ms_sg_use_debug" id="ms-sg-use-debugging" value="1" <?= $checked ?>>
                                        </label>
                                    </fieldset>
                                </td>
                            </tr>
                            <?php
                            // From Add-On
                            do_action('media_sync_extended_advanced_options_render');
                            ?>
                        </tbody>
                    </table>

                    <?php submit_button(); ?>
                </form>
            </div>
            <?php
        }


        /**
         * Render option for selecting post date
         *
         * @since 1.1.0
         * @param string $name Field name
         * @return void
         */
        static public function media_sync_render_post_date_option($name)
        {
            $selected = get_option( 'ms_sg_file_post_date' );

            if(isset($_GET['file_post_date'])) { 
                $selected = $_GET['file_post_date'];
            }
            ?>
            <fieldset>
                <div class="import-option-date-type-item">
                    <label>
                        <input type="radio" name="<?= $name ?>" value="default" <?= checked( 'default', $selected ) ?>/>
                        <span><?= __('Default', 'media-sync') ?></span>
                    </label>
                    <p class="description"><?= __('Defaults to what WordPress uses - current time.', 'media-sync') ?></p>
                </div>
                <div class="import-option-date-type-item">
                    <label>
                        <input type="radio" name="<?= $name ?>" value="file_time" <?= checked( 'file_time', $selected ) ?>/>
                        <span><?= __('File time', 'media-sync') ?></span>
                    </label>
                    <p class="description"><?= __('File modification timestamp - when it was last modified.', 'media-sync') ?></p>
                </div>
                <div class="import-option-date-type-item">
                    <label>
                        <input type="radio" name="<?= $name ?>" value="smart_file_time" <?= checked( 'smart_file_time', $selected ) ?>/>
                        <span><?= __('Smart file time', 'media-sync') ?></span>
                    </label>
                    <p class="description"><?= sprintf(__('If file timestamp does not match the folder in which it is - it will construct the date/time based on the folder: %s.', 'media-sync'), '{year}-{month}-01 00:00:00') ?></p>
                </div>
            </fieldset>
            <?php
        }


        /**
         * Render table header and footer
         *
         * @since 0.1.0
         * @param string $tag [thead|tfoot]
         * @param string $dir
         * @return void
         */
        static public function media_sync_render_thead_tfoot_row($tag, $dir = null)
        {
            $cb_id = 'cb-select-all-' . ($tag == 'thead' ? '1' : '2');
            ?>
            <<?= $tag ?>>
                <tr>
                    <td class="manage-column check-column check-column-all"<?= $tag == 'thead' ? ' id="cb"':''?>>
                        <label class="screen-reader-text" for="<?= $cb_id ?>"><?= __('Select All', 'media-sync') ?></label>
                        <input id="<?= $cb_id ?>" type="checkbox">
                    </td>
                    <th scope="col" class="manage-column column-title column-primary"<?= $tag == 'thead' ? ' id="title"':''?>>
                        <span><?= __('File', 'media-sync') ?> <?= $dir ? (__('at', 'media-sync') . ' ' . $dir) : '' ?></span>
                    </th>
                </tr>
            </<?= $tag ?>>
            <?php
        }


        /**
         * Render table row for each file or directory
         *
         * @since 0.1.0
         * @param array $item
         * @return void
         */
        static public function media_sync_render_row($item)
        {
            $has_file_id = isset($item['file_id']) && $item['file_id'] !== false;
            $url = $has_file_id ? esc_url(add_query_arg(array('post' => $item['file_id'], 'action' => 'edit'), get_admin_url(null, 'post.php'))) : $item['url'];
            $url_attr = $item['is_dir'] !== true ? ' target="_blank"' : '';
            $count_children = $item['count_children'];

            $cls = 'media-sync-list-file';
            $cls .= ' is-' . ($item['is_dir'] === true ? 'dir' : 'file');
            $cls .= ' level-' . $item['level'];
            $cls .= ' is-first-level-' . ($item['level'] === 1 ? 'yes' : 'no');
            if ($item['is_dir'] !== true) {
                $cls .= ' is-in-db-' . ($has_file_id ? 'yes' : 'no');
            }

            // This can be made optional, for a bit different UI
            $toggle_arrows = true;
            if ($toggle_arrows) {
                $is_link = $item['is_dir'] !== true;
                $cls .= ' toggle-arrows-yes';
            } else {
                $is_link = $item['is_dir'] !== true || $count_children > 0;
                $url_attr .= ' class="js-toggle-row"';
            }

            $is_trash = isset($item['file_status']) && $item['file_status'] === 'trash';

            $row_id = "media-sync-item-" . $item['alias'];
            ?>

            <tr class="<?= $cls ?>" id="<?= $row_id ?>" data-parent-id="media-sync-item-<?= $item['parent_alias'] ?>">
                <th scope="row" class="check-column">
                    <label class="screen-reader-text" for="cb-select-<?= $item['alias'] ?>"></label>
                    <input type="checkbox" class="js-checkbox" id="cb-select-<?= $item['alias'] ?>"
                           value="<?= $item['absolute_path'] ?>" data-row-id="<?= $row_id ?>">
                </th>
                <td class="title column-title has-row-actions column-primary" data-colname="<?= __('File', 'media-sync') ?>">
                    <?php if (!empty($item['parents'])) : ?>
                        <span class="media-sync-parents">
                            <?php foreach ($item['parents'] as $parent_key => $parent) : ?>
                                <?php
                                $parent_cls = 'media-sync-parent';
                                $parent_cls .= ' is-first-' . ($parent_key == 0 ? 'yes' : 'no');
                                $parent_cls .= ' is-last-' . ($parent_key + 1 == count($item['parents']) ? 'yes' : 'no');
                                ?>
                                <span class="<?= $parent_cls ?>"><i></i></span>
                            <?php endforeach; ?>
                            <span class="clearfix"></span>
                        </span>
                    <?php endif; ?>

                    <?php if ($toggle_arrows && $item['is_dir'] === true) : ?>
                        <span class="js-toggle-row media-sync-toggle-row dashicons"></span>
                    <?php endif; ?>

                    <?= $is_link ? '<a href="' . $item['url'] . '"' . $url_attr . '>' : '' ?>
                    <?php if ($item['is_dir'] === true) : ?>
                        <span class="dashicons dashicons-category"></span>
                    <?php endif; ?>
                        <span class="media-sync-file-name">
                            <?= $item['display_name'] ?>
                        </span>
                    <?= $is_link ? '</a>' : '' ?>

                    <?php if ($item['is_dir'] === true && $count_children !== null) : ?>
                        <span class="media-sync-num-items"><?= sprintf('(%u %s)', $count_children, $count_children == 1 ? __('item', 'media-sync') : __('items', 'media-sync')) ?></span>
                    <?php endif; ?>

                    <?php if ($has_file_id) : ?>
                        <span class="media-sync-already-in-db"> - <?= __('Already in', 'media-sync') ?>
                            <a href="<?= $url ?>" class="dashicons dashicons-admin-media" target="_blank"></a>
                            <?= $is_trash ? ' (' . __('In Trash', 'media-sync') . ')' : '' ?>
                        </span>
                    <?php endif; ?>
                </td>
            </tr>

            <?php
            if (!empty($item['children'])) :
                foreach ($item['children'] as $child_item) :
                    self::media_sync_render_row($child_item);
                endforeach;
            endif;
        }


        /**
         * Ajax action to import selected files
         *
         * @since 0.1.0
         * @return string
         */
        static public function media_sync_import_files()
        {
            if(!MediaSync::media_sync_user_has_general_access()) {
                wp_die(__('You do not have sufficient permissions to access this page.', 'media-sync'));
            }

            check_ajax_referer( 'media_sync_import_files', 'security' );

            // Get database stuff
            global $wpdb;

            $results = array();

            if(isset($_POST['media_items']) && !empty($_POST['media_items'])) {

                // Make sure that this file is included, as wp_generate_attachment_metadata() depends on it.
                require_once( ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'image.php' );


                $files_in_db = self::media_sync_get_files_in_db();


                $post_date_type = isset($_POST['file_post_date']) ? $_POST['file_post_date'] : get_option( 'ms_sg_file_post_date', 'default' );
                $dry_run = isset($_POST['dry_run']) && json_decode($_POST['dry_run']) === true;

                foreach ($_POST['media_items'] as $media_item) {

                    if(isset($media_item['file']) && !empty($media_item['file'])) {

                        // This comes from JS and it's taken from checkbox value, which is $item['absolute_path'] from media_sync_get_list_of_files()
                        $absolute_path = urldecode($media_item['file']);
                        $relative_path = self::media_sync_get_relative_path($absolute_path);

                        // It's quicker to get all files already in db and check that array, than to do this query for each file
                        // $query = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE guid LIKE '%{$relative_path}'";
                        // $is_in_db = intval($wpdb->get_var($query)) > 0;

                        $is_in_db = isset($files_in_db[$relative_path]) && !empty($files_in_db[$relative_path]);

                        // Check if file is already in database
                        if(!$is_in_db) {

                            // Prepare data to be saved to `wp_posts` and `wp_postmeta`
                            $attachment = self::media_sync_prepare_attachment_data($absolute_path, $relative_path, $post_date_type);

                            if (!$attachment || isset($attachment['error']) || isset($attachment['errorMessage'])) {
                                // It's probably better to just continue importing other files
                                // echo json_encode($attachment);
                                // wp_die();

                                // This will mark this file as failed and show error below file name
                                $results[] = array(
                                    'row_id' => $media_item['row_id'],
                                    'inserted' => false,
                                    'error' => isset($attachment['error']) ? $attachment['error'] : null,
                                    'errorMessage' => isset($attachment['errorMessage']) ? $attachment['errorMessage'] : null
                                );
                                continue;
                            }

                            // If we're actually importing (creating database records)
                            if(!$dry_run) {

                                // Import file to database (`wp_posts` and `wp_postmeta`)
                                $import_response = self::media_sync_update_import_to_database($attachment, $absolute_path);

                                // Response is an array if there are errors or true if success
                                $is_inserted = $import_response === true;

                                // It's probably better to just continue importing other files
                                //if (!$is_inserted) {
                                //    echo json_encode($import_response);
                                //    wp_die();
                                //}

                                $results[] = array(
                                    'row_id' => $media_item['row_id'],
                                    'inserted' => $is_inserted,
                                    'error' => isset($import_response['error']) ? $import_response['error'] : null,
                                    'errorMessage' => isset($import_response['errorMessage']) ? $import_response['errorMessage'] : null
                                );
                            } else {
                                $results[] = array(
                                    'row_id' => $media_item['row_id'],
                                    'inserted' => true
                                );
                            }
                        }
                    }
                }
            }

            if (self::media_sync_is_debug()) {
                foreach ($results as $result) {
                    echo self::media_sync_render_html_import_result($result);
                }
            } else {
                echo json_encode(array(
                    'results' => $results
                ));
            }

            wp_die(); // Must have for Ajax calls
        }

        /**
         * Render HTML response for each item.
         * Used only when debugging is turned on.
         *
         * @since 1.2.6
         * @param array $result
         * @return void
         */
        static public function media_sync_render_html_import_result($result)
        {
            $icon = isset($result['inserted']) && $result['inserted'] === true ? "✅" : "❌";
            $row_id = isset($result['row_id']) ? $result['row_id'] : "";
            $errors = "";
            if (isset($result['error']) && !empty($result['error'])) {
                $errors .= "  [Error]: {$result['error']} ";
            }
            if (isset($result['errorMessage']) && !empty($result['errorMessage'])) {
                $errors .= (!empty($errors) ? "<br />" : "") . "  [Error Message]: {$result['errorMessage']} ";
            }

            $return = "<div class='media-sync-html-response-item-" . (!empty($errors) ? 'error' : 'success') . "'>";
            $return .= "{$icon} {$row_id}" . (!empty($errors) ? '<div>' . $errors . '</div>' : '');
            $return .= "</div>";

            return $return;
        }

        /**
         * Find mime type for file being imported
         *
         * @since 1.2.3
         *
         * @param string $absolute_path Absolute path to the file being imported
         * @return string|array
         */
        static public function media_sync_get_mime_type($absolute_path)
        {
            try {
                $mime_type = '';
                if (function_exists('mime_content_type')) {
                    $mime_type = mime_content_type($absolute_path);
                }

                if (!empty($mime_type)) {
                    return $mime_type;
                }

                $file_type = wp_check_filetype(basename($absolute_path), null);

                if(!$file_type['type']) {
                    return array(
                        'errorMessage' => sprintf(__('Invalid mime type for file: %s.', 'media-sync'), $absolute_path),
                    );
                }

                return $file_type['type'];

            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error getting mime type for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }
        }

        /**
         * Prepare data to be imported to `wp_posts` table
         *
         * @since 1.2.2
         *
         * @param string $absolute_path Absolute path to the file being imported
         * @param string $relative_path Path relative to the site URL
         * @param string $post_date_type How to generate post date
         * @return array
         */
        static public function media_sync_prepare_attachment_data($absolute_path, $relative_path, $post_date_type)
        {
            try {
                $mime_type = self::media_sync_get_mime_type($absolute_path);

                if (isset($mime_type['error']) || isset($mime_type['errorMessage'])) {
                    return $mime_type;
                }

                // Prepare an array of post data for the attachment.
                $attachment = array(
                    'guid'           => get_site_url() . $relative_path,
                    'post_mime_type' => $mime_type,
                    'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $relative_path ) ),
                    'post_content'   => '',
                    'post_status'    => 'inherit'
                );

                // Try to get post date based on settings - "Default" option will not set anything, so WP can use defaults
                $post_date = self::media_sync_post_date($absolute_path, $post_date_type);

                if(!empty($post_date)) {
                    $attachment['post_date'] = $post_date;
                    $attachment['post_date_gmt'] = $post_date;
                }

                return $attachment;
            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error preparing attachment data for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }
        }


        /**
         * Import file to database (`wp_posts` and `wp_postmeta`)
         *
         * @since 1.2.1
         *
         * @param array $attachment Data for `wp_posts` table
         * @param string $absolute_path Absolute path to the file being imported
         * @return array|true
         */
        static public function media_sync_update_import_to_database($attachment, $absolute_path)
        {
            if (!stream_resolve_include_path($absolute_path)) {
                return array(
                    'errorMessage' => sprintf(__('File to import not found at path: %s.', 'media-sync'), $absolute_path)
                );
            }

            // Insert the attachment (`wp_posts` table)
            try {
                $max_execution_time = ini_get('max_execution_time');
                if ($max_execution_time) {
                    // Reset execution time so that each file can be processed
                    set_time_limit($max_execution_time);
                }

                $attach_id = wp_insert_attachment($attachment, $absolute_path);
            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error inserting attachment (`wp_posts` table) for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }

            if (!($attach_id > 0)) {

                // TODO: Revert wp_insert_attachment()?

                return array(
                    'errorMessage' => sprintf(__('Attach ID not received for inserted attachment (`wp_posts` table) for file: %s.', 'media-sync'), $absolute_path)
                );
            }

            try {
                // Create custom meta tag (in `wp_postmeta` table) so that we later know this file was imported using this plugin
                $media_sync_metadata = 1;
                $media_sync_metadata = apply_filters('media_sync_filter_before_update_msc_metadata', $media_sync_metadata, $attach_id, $absolute_path);
                if ($media_sync_metadata) {
                    add_post_meta($attach_id, '_msc', $media_sync_metadata, true);
                }
            } catch (Exception $e) {}

            try {
                // Generate the metadata for the attachment
                $attach_data = wp_generate_attachment_metadata($attach_id, $absolute_path);
            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error generating attachment metadata (`wp_postmeta` table) for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }

            if (!$attach_data || empty($attach_data)) {
                return array(
                    'errorMessage' => sprintf(__('Attachment metadata could not be generated from %s.', 'media-sync'), $absolute_path),
                    'error' => json_encode($attachment)
                );
            }

            try {
                /**
                 * Apply this filter to collect additional metadata and/or to run some additional actions,
                 * e.g. to "auto-connect" items to pages, posts, and WooCommerce products.
                 * Returning empty data will skip updating metadata (wp_update_attachment_metadata),
                 * so this filter can also be used to totally overwrite updating attachment metadata.
                 *
                 * This filter can be used in a number of different ways:
                 * 1. to collect additional metadata,
                 * 2. to run some additional custom actions
                 *   (e.g. to "auto-connect" items to pages, posts, and WooCommerce products),
                 * 3. to skip or completely overwrite updating the metadata (`wp_update_attachment_metadata` function),
                 *   when this filter returns empty data (null).
                 *
                 * NOTE:
                 * Things could break if the filter doesn't return proper attach data
                 * and it hasn't created/updated the `wp_postmeta` table record (meta_key _wp_attachment_metadata).
                 *
                 * @since 1.2.5
                 *
                 * @param array $attach_data Data received from WP function: wp_generate_attachment_metadata
                 * @param int $attach_id
                 * @return array|null
                 */
                $attach_data = apply_filters('media_sync_filter_before_update_metadata', $attach_data, $attach_id);
            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error processing filter media_sync_filter_before_update_metadata for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }

            // There is this same validation (if) before "media_sync_filter_before_update_metadata" filter,
            // so if we end up here, it's because of that filter. Which either already updated metadata,
            // or doesn't want to update it (basically telling us not to run wp_update_attachment_metadata).
            if (!$attach_data || empty($attach_data)) {
                return true;
            }

            try {
                // Update `wp_postmeta` database record
                wp_update_attachment_metadata($attach_id, $attach_data);
            } catch (Exception $e) {
                return array(
                    'errorMessage' => sprintf(__('Error updating attachment metadata (`wp_postmeta` table) for file: %s.', 'media-sync'), $absolute_path),
                    'error' => $e->getMessage()
                );
            }

            return true;
        }


        /**
         * Returns post date for file being imported based on selected option
         * 
         * @since 1.1.0
         * @param string $file_path Absolute path to file being imported
         * @param string $type Selected option telling which date to find [default|file_time|smart_file_time]
         * @return string
         */
        static public function media_sync_post_date($file_path, $type)
        {
            // Default is empty string so that WordPress can set its default value (current date)
            $post_date = '';

            try {

                // Take "time modified" of file being imported
                $file_timestamp = filemtime( $file_path );
                if($file_timestamp) {

                    // Convert to datetime
                    $file_time = date( 'Y-m-d H:i:s', $file_timestamp );
                    if($file_time) {

                        // For "File time" option - return file time
                        if($type === 'file_time') {
                            $post_date = $file_time;
                        }

                        // For "Smart file time" option - determine post date by comparing file's modified date with folder in which it is
                        if($type === 'smart_file_time') {
                            // Use file time as a fallback
                            $post_date = $file_time;

                            // Compare the file date with the folder date structure
                            // Try to extract year and month from file path (e.g. "2020/02" or "2020\02")
                            // But this path is now always using forward slash (even on Windows Server), so we can again use just '/(\d{4})\/(\d{2})/'
                            preg_match('/(\d{4})[\/|\\\\](\d{2})/', $file_path, $matches);
                            if ( !empty($matches) ) {
                                $year_and_month = $matches[1] . '-' . $matches[2];
                                $folder_time = $year_and_month . '-' . '01' . ' 00:00:00';

                                // If what we found for folder date is a date (double check for regex above)
                                if ( date('Y-m-d H:i:s', strtotime( $folder_time )) == $folder_time ) {

                                    // If file's year and month values do not match with folder's year and month
                                    if ( date( 'Y-m', $file_timestamp ) != $year_and_month ) {
                                        // Use the folder date instead
                                        $post_date = $folder_time;
                                    }
                                }
                            }
                        }
                    }
                }


                // Receive external function for setting custom post date
                $external = apply_filters('media_sync_filter_post_date', $post_date, $file_path, $type );

                // Return received custom post date if it's empty string or a date
                if( is_string($external) && ($external === '' || ( date('Y-m-d H:i:s', strtotime( $external )) == $external) ) ) {
                    $post_date = $external;
                }

                return $post_date;
            } catch ( Exception $e ) {
                return $post_date;
            }
        }


        /**
         * Get path of directory used by this plugin as main import directory (uploads dir).
         *
         * Since PHP on Windows Server recognizes both backslash ("\") or forward slash ("/"),
         * and WordPress always uses forward slashes (in it's built in functions), we'll also always use forward slashes.
         *
         * So "C:\www\WP/wp-content/uploads" becomes "C:/www/WP/wp-content/uploads"
         *
         * Returning for example:
         * * /var/www/WP/wp-content/uploads
         * * C:/www/WP/wp-content/uploads
         *
         * @since 1.1.3
         * @param boolean $append_sub_dir Should we append sub dir from settings
         * @return string
         */
        static private function media_sync_get_uploads_basedir($append_sub_dir = true)
        {
            $upload_dir = wp_upload_dir();

            if(!($upload_dir && isset($upload_dir['basedir']) && !empty($upload_dir['basedir']))) {
                return null;
            }

            // Convert backslashes ("\") to forward slashes ("/")
            $basedir = wp_normalize_path($upload_dir['basedir']);

            // Append sub dir from settings
            $sub_dir = get_option( 'ms_sg_scan_sub_dir' );
            if($append_sub_dir === true && $sub_dir && !empty($sub_dir)) {

                // Add one forward slash between base dir and sub dir
                $base_with_sub = $basedir . self::MEDIA_SYNC_DS . trim($sub_dir);

                // Return only if exists
                if( stream_resolve_include_path($base_with_sub) ) {
                    return $base_with_sub;
                } else {
                    return null;
                }
            }

            return $basedir;
        }


        /**
         * Get path absolute to WP root. Always using forward slashes ("/").
         *
         * e.g. /var/www/WP/wp-content/uploads -> /wp-content/uploads
         * e.g. C:/www/WP/wp-content/uploads -> /wp-content/uploads
         *
         * @since 1.1.3
         * @param string $absolute_path Absolute file path. Will be convert to forward slashes ("/").
         * @return string
         */
        static private function media_sync_get_relative_path($absolute_path)
        {
            // Since get_home_path() and WP in general always use forward slashes, we need to convert it as well
            $absolute_path = wp_normalize_path($absolute_path);

            // Always using forward slash ("/")
            return str_replace(get_home_path(), self::MEDIA_SYNC_DS, $absolute_path);
        }


        /**
         * Scan "uploads" directory and return recursive list of files and directories
         *
         * @since 0.1.0
         * @return Generator
         */
        static private function media_sync_get_list_of_uploads()
        {
            self::$start_time = microtime(true);

            self::$upload_dir_path = self::media_sync_get_uploads_basedir();
            if(!self::$upload_dir_path) {
                return array();
            }

            // Limit scanning to specific sub folder or encoded path (e.g. &sub_dir=2020%2F01)
            if(isset($_GET['sub_dir']) && !empty($_GET['sub_dir'])) {
                // Since this path is always using forward slashes, we're also using forward slash ("/")
                self::$upload_dir_path = self::$upload_dir_path . self::MEDIA_SYNC_DS . urldecode($_GET['sub_dir']);
            }

            if(empty(self::$files_in_db)) {
                self::$files_in_db = self::media_sync_get_files_in_db();
            }

            $associated_filter = isset($_GET['associated-filter']) && !empty($_GET['associated-filter']) ? explode(':', urldecode($_GET['associated-filter'])) : null;


            // Clear cached files (affecting file_exists, stream_resolve_include_path, etc.)
            clearstatcache();

            // Show debugging stats if debugging is turned on
            self::media_sync_render_debug_stats('before uploads directory scan');

            // Get all files - returning Generator (not Array)
            $tree = self::media_sync_get_list_of_files(self::$upload_dir_path, self::$files_in_db, $associated_filter);

            // Show debugging stats if debugging is turned on
            self::media_sync_render_debug_stats('after uploads directory scan');

            return $tree;
        }


        /**
         * Scan directory (passed as first value) and return recursive list of files and directories
         *
         * @since 0.1.0
         * @param string $current_dir_path Changing recursively for each directory that gets iterated
         * @param array $files_in_db List of files that are already in database
         * @param string $associated_filter Filter by "association", for now it can only be "file missing from media library" (solved by "Import Selected")
         * @return Generator
         */
        static private function media_sync_get_list_of_files($current_dir_path, $files_in_db, $associated_filter)
        {
            if(!self::$upload_dir_path) {
                yield null;
            }

            // "stream_resolve_include_path" is like "file_exists" but should be quicker
            if( !stream_resolve_include_path($current_dir_path) ) {
                yield null;
            }

            foreach (scandir($current_dir_path) as $key => $file_name) {
                // Skip ".", "..", etc.
                if ($file_name == '.' || $file_name == '..' || $file_name == ".DS_Store" || $file_name == ".htaccess" || $file_name == "index.php") {
                    continue;
                }

                // Convert backslashes ("\") to forward slashes ("/")
                $full_path = wp_normalize_path($current_dir_path . "/" . $file_name);

                $isDir = is_dir($full_path);

                // Default file skipping rules:
                // 1. contains image size at the end (e.g. -100x100.jpg), or
                // 2. ends with "-scaled" (also WP generated), or
                // 3. retina thumbnail (e.g. -100x100@2x.jpg), or
                // 4. has .webp after another file type (e.g. .jpg.webp).
                $is_ignored = preg_match('/(-scaled|[_-]\d+x\d+)|@[2-6]\x(?=\.[a-z]{3,4}$)|\.[a-z]{3,4}.webp/im', $file_name) == true;

                // Receive external rules for skipping files/folders
                $is_ignored_external = apply_filters('media_sync_filter_is_scan_object_ignored', $is_ignored, $full_path, $file_name);

                // Take custom rule for skipping files/folders from external hook/filter function
                if (is_bool($is_ignored_external)) {
                    $is_ignored = $is_ignored_external;
                }


                if ($is_ignored) {
                    continue;
                }


                $relative_path = self::media_sync_get_relative_path($full_path);
                $file_in_db = isset($files_in_db[$relative_path]) && !empty($files_in_db[$relative_path]) ?
                    $files_in_db[$relative_path] : false;
                $file_id = $file_in_db && !empty($file_in_db['id']) ? $file_in_db['id'] : false;
                $file_status = $file_in_db && !empty($file_in_db['status']) ? $file_in_db['status'] : false;

                if (!$isDir && $associated_filter && $associated_filter[0] == 'missing_from' && $associated_filter[1] == 'media_library' && $file_id !== false) {
                    continue;
                }


                // Ugly, but efficient
                $children = array();

                if ($isDir) {
                    $children = self::media_sync_get_list_of_files($full_path, $files_in_db, $associated_filter);

                    if (empty($children)) {
                        continue;
                    }
                }


                $file_uid = uniqid('', true);

                // Get current parents, to get for example: "/2012/03"
                $parents_path = str_replace(self::$upload_dir_path, '', $current_dir_path);

                // Trim first slash, to get for example: "2012/03"
                $parents_path = ltrim($parents_path, self::MEDIA_SYNC_DS);

                // Since this path is always using forward slashes, that's what we'll use here
                $parents = !empty($parents_path) ? explode(self::MEDIA_SYNC_DS, $parents_path) : array();

                // Replace forward slash with "_" and do other cleanup since this will be used as HTML attribute
                $parent_alias = !empty($parents_path) ? sanitize_title(str_replace(self::MEDIA_SYNC_DS, '_', $parents_path), $file_uid) : '';

                $alias = sanitize_title($file_name, $file_uid);

                $item = array(
                    'alias' => (!empty($parent_alias) ? $parent_alias . '_' : '') . $alias,
                    'display_name' => $file_name,
                    'is_dir' => !!$isDir,
                    'level' => count($parents) + 1,
                    'parent_alias' => $parent_alias,
                    'parents' => $parents,
                    'absolute_path' => self::media_sync_url_encode($full_path)
                );

                if ($isDir) {
                    $item['url'] = 'javascript:;';
                    $item['children'] = $children;
                    // TODO: Figure out how to count children when using generators (yield)
                    $item['count_children'] = null;
                } else {
                    // Encode just file name
                    $item['url'] = get_site_url() . self::media_sync_url_encode($relative_path);
                    $item['file_id'] = $file_id;
                    $item['file_status'] = $file_status;
                    $item['children'] = [];
                    $item['count_children'] = 0;
                }

                yield $item;
            }
        }

        /**
         * Convert special characters to safe characters but keeping forward slash.
         *
         * @param $url
         * @return string
         * @since 1.2.4
         */
        static private function media_sync_url_encode($url)
        {
            return str_replace('%2F', '/', rawurlencode($url));
        }


        /**
         * Get list of files that are already in database
         *
         * Caching does not seem to work, disabled for now
         *
         * @since 0.1.0
         * @return array
         */
        static private function media_sync_get_files_in_db()
        {
            $media_query = new WP_Query(array(
                'post_type' => 'attachment',
                'post_status' => array('inherit', 'trash'),
                'posts_per_page' => -1
            ));

            $files = array();
            foreach ($media_query->posts as $post) {

                $file_url = '';
                if(function_exists('wp_get_original_image_url')) {
                    // First try to find original image URL, to get proper URL without "-scaled" part for bigger images
                    // https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
                    $file_url = wp_get_original_image_url($post->ID);
                }

                // If not image, get attachment URL
                if(!$file_url || empty($file_url)) {
                    $file_url = wp_get_attachment_url($post->ID);
                }

                // Should already have forward slashes since it's URL
                $relative_path = parse_url($file_url, PHP_URL_PATH);

                $files[$relative_path] = array(
                    'id' => $post->ID,
                    'name' => $post->post_title,
                    'status' => $post->post_status
                );
            }

            return $files;
        }


        /**
         * Show debugging stats.
         *
         * @since 1.2.0
         * @param string $event When are we showing these stats
         * @return void
         */
        static public function media_sync_render_debug_stats($event)
        {
            if(!$event || !self::media_sync_is_debug()) {
                return null;
            }

            $mem_usage = memory_get_usage();
            $mem_limit = self::media_sync_get_memory_limit();
            $limit_percentage = $mem_usage * 100 / $mem_limit;
            ?>
            <div class="media-sync-debugging-stats">
                <div class="media-sync-debugging-title">Debugging <strong><?= $event ?></strong>:</div>
                <div class="media-sync-debugging-stat-row">
                    Peak memory usage: <?= memory_get_peak_usage(true) / 1024 / 1024 ?> MB / <?= memory_get_peak_usage(true) / 1024 ?> KB
                </div>
                <div class="media-sync-debugging-stat-row">
                    Memory usage: <?= round($mem_usage / 1024 / 1024) ?> MB / <?= round($mem_usage / 1024) ?> KB
                    (<?= round($limit_percentage) ?>% of the available memory: <?= round($mem_limit / 1024 / 1024) ?> MB / <?= round($mem_limit / 1024) ?> KB)
                </div>
                <div class="media-sync-debugging-stat-row">
                    Time since start: <?= self::$start_time > 0 ? microtime(true) - self::$start_time : 0 ?> seconds
                </div>
            </div>
            <?php
        }


        /**
         * Parse the memory_limit variable from the php.ini file.
         *
         * @since 1.2.0
         * @return int
         */
        static private function media_sync_get_memory_limit()
        {
            $limit_string = ini_get('memory_limit');
            $unit = strtolower(mb_substr($limit_string, -1 ));
            $bytes = intval(mb_substr($limit_string, 0, -1), 10);

            switch ($unit)
            {
                case 'k':
                    $bytes *= 1024;
                    break 1;

                case 'm':
                    $bytes *= 1024 * 1024;
                    break 1;

                case 'g':
                    $bytes *= 1024 * 1024 * 1024;
                    break 1;

                default:
                    break 1;
            }

            return $bytes;
        }


        /**
         * Should debugging be used.
         *
         * @since 1.2.0
         * @return boolean
         */
        static public function media_sync_is_debug()
        {
            if(isset($_GET['debug'])) {
                return $_GET['debug'] !== "0";
            }

            if(!!get_option('ms_sg_use_debug', 1)) {
                return true;
            }

            // return defined('WP_DEBUG') && WP_DEBUG;

            return false;
        }

        
        /**
         * Check if logged in user has access
         *
         * @since 1.1.0
         * @return boolean
         */
        static public function media_sync_user_has_general_access() 
        {
            if ( !current_user_can( 'upload_files' ) ) {
                return false;
            }

            return true;
        }
    }
endif; // End if class_exists check.
?>