<?php
namespace SabaiApps\Directories\Component\CSV\SystemTool;

use SabaiApps\Directories\Component\System\Tool\AbstractTool;
use SabaiApps\Directories\Component\Form\Form;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Exception;
use SabaiApps\Directories\Component\Field\IField;
use SabaiApps\Directories\Component\Entity\Model\Bundle;

class ImportSystemTool extends AbstractTool {

	protected $_csvFile;

	protected function _systemToolInfo() {
		return array(
			'label'       => _x( 'Import content', 'system tool name', 'directories-pro' ),
			'description' => '',
			'weight'      => 60,
			'btn_label'   => _x( 'Import', 'directories-pro' ),
			'steps'       => array(
				'upload_csv',
				'map_fields',
				'importer_settings',
			),
			'hidden'      => true,
		);
	}

	protected function _getBundle( Context $context ) {
		return isset( $context->bundle ) && ( $bundle = $this->_application->Entity_Bundle( $context->bundle ) ) ? $bundle : false;
	}

	public function systemToolSettingsForm( $step, array $values, array $parents = array(), ?Context $context = null ) {
		if ( ! $bundle = $this->_getBundle( $context ) ) {
			return false;
		}

		switch ( $step ) {
			case 'upload_csv':
				$bundle                = $this->_getBundle( $context );
				$existing_files        = $this->_application->Filter( 'csv_import_files', array(), array( $bundle ) );
				$custom_existing_files = array();
				$dir                   = $this->_application->getComponent( 'CSV' )->getVarDir();
				if ( is_dir( $dir ) ) {
					foreach ( glob( $dir . '/*.csv' ) as $csv_file ) {
						$custom_existing_files[ $csv_file ] = basename( $csv_file );
					}
				}
				$form = array(
					'upload'    => array(
						'#type'                 => 'file',
						'#description'          => sprintf(
							__( 'You can also upload your CSV file to %s using FTP, and then reload this page.', 'directories-pro' ),
							$this->_application->getComponent( 'CSV' )->getVarDir()
						),
						'#upload_dir'           => $this->_application->getComponent( 'System' )->getTmpDir(),
						'#allowed_extensions'   => array( 'csv' ),
						'#required'             => function ( $form ) use ( $parents ) {
							return $form->getValue( array_merge( $parents, array( 'type' ) ) ) === 'upload'; },
						// The finfo_file function used by the uploader to check mime types for CSV files is buggy. We can skip it safely here since this is for admins only.
						'#skip_mime_type_check' => true,
						'#multiple'             => false,
						'#states'               => array(
							'visible' => array(
								sprintf( '[name="%s"]', $this->_application->Form_Form_fieldName( array_merge( $parents, array( 'type' ) ) ) ) => array( 'value' => 'upload' ),
							),
						),
						'#weight'               => 5,
					),
					'delimiter' => array(
						'#type'          => 'textfield',
						'#field_prefix'  => __( 'Delimiter', 'directories-pro' ),
						'#size'          => 5,
						'#description'   => __( 'Enter the character used as CSV column delimiters.', 'directories-pro' ),
						'#min_length'    => 1,
						'#max_length'    => 1,
						'#default_value' => ',',
						'#required'      => function ( $form ) use ( $parents ) {
							return $form->getValue( array_merge( $parents, array( 'type' ) ) ) === 'upload'; },
						'#weight'        => 6,
					),
					'enclosure' => array(
						'#type'          => 'textfield',
						'#field_prefix'  => __( 'Enclosure', 'directories-pro' ),
						'#size'          => 5,
						'#description'   => __( 'Enter the character used as CSV column enclosures.', 'directories-pro' ),
						'#min_length'    => 1,
						'#max_length'    => 1,
						'#default_value' => '"',
						'#required'      => function ( $form ) use ( $parents ) {
							return $form->getValue( array_merge( $parents, array( 'type' ) ) ) === 'upload'; },
						'#weight'        => 7,
					),
				);

				if ( ! empty( $existing_files )
					|| ! empty( $custom_existing_files )
				) {
					$form['type']     = array(
						'#type'          => 'select',
						'#title'         => __( 'CSV file', 'directories-pro' ),
						'#options'       => array(
							'upload'   => __( 'Upload a CSV file', 'directories-pro' ),
							'existing' => __( 'Select from existing CSV files', 'directories-pro' ),
						),
						'#default_value' => 'upload',
						'#required'      => true,
						'#weight'        => 1,
					);
					$form['existing'] = array(
						'#type'             => 'select',
						'#description'      => __( 'Select a CSV file to import.', 'directories-pro' ),
						'#options'          => $existing_files + $custom_existing_files,
						'#weight'           => 5,
						'#states'           => array(
							'visible' => array(
								sprintf( '[name="%s"]', $this->_application->Form_Form_fieldName( array_merge( $parents, array( 'type' ) ) ) ) => array( 'value' => 'existing' ),
							),
						),
						'#required'         => function ( $form ) use ( $parents ) {
							return $form->getValue( array_merge( $parents, array( 'type' ) ) ) === 'existing'; },
						'#element_validate' => array(
							array( array( $this, '_validateExistingFile' ), array( $parents ) ),
						),
					);
				} else {
					$form['upload']['#title'] = __( 'CSV file', 'directories-pro' );
					$form['type']             = array(
						'#type'          => 'hidden',
						'#default_value' => 'upload',
					);
				}

				foreach ( array_keys( $form ) as $key ) {
					if ( strpos( $key, '#' ) !== 0 ) {
						$form[ $key ]['#horizontal'] = true;
					}
				}

				return $form;

			case 'map_fields':
				$bundle = $this->_getBundle( $context );

				// Extract header and a first row
				$rows        = $this->_getCsvRows( $bundle, $values['upload_csv'], 2 );
				$csv_columns = $rows[0];
				$csv_row1    = $rows[1];

				$options                 = array();
				$optgroups               = $custom_field_options = array();
				$importers_by_field_type = $this->_application->CSV_Importers( true );
				$fields                  = $this->_application->Entity_Field( $bundle->name );
				$properties              = $this->_application->Entity_Types_impl( $bundle->entitytype_name )->entityTypeInfo( 'properties' );
				$id_column_field_type    = $properties['id']['type'];
				$id_column_key           = null;
				foreach ( $fields as $field_name => $field ) {
					$field_type    = $field->getFieldType();
					$importer_name = isset( $importers_by_field_type[ $field_type ] ) ? $importers_by_field_type[ $field_type ] : null;
					if ( ! $importer_name
						|| ( ! $importer = $this->_application->CSV_Importers_impl( $importer_name, true ) )
						|| ! $importer->csvImporterSupports( $bundle, $field )
					) {
						continue;
					}

					$columns = $importer->csvImporterColumns( $field );
					if ( ! empty( $columns ) ) {
						$optgroups[ $field_name ] = $this->_getFieldLabel( $field );
						if ( $field->isCustomField() ) {
							foreach ( $columns as $column => $label ) {
								$custom_field_options[ $this->_getFieldOptionValue( $field_name, $column ) ] = array(
									'#group' => $field_name,
									'#title' => $label,
								);
							}
						} else {
							foreach ( $columns as $column => $label ) {
								$options[ $this->_getFieldOptionValue( $field_name, $column ) ] = array(
									'#group' => $field_name,
									'#title' => $label,
								);
							}
						}
					} else {
						$option = $this->_getFieldOptionValue( $field_name, '' );
						if ( $field->isCustomField() ) {
							$custom_field_options[ $option ] = $this->_getFieldLabel( $field );
						} else {
							$options[ $option ] = $this->_getFieldLabel( $field );
							if ( $field_type === $id_column_field_type ) {
								$id_column_key = $option;
							}
						}
					}
				}
				asort( $options );
				if ( ! empty( $custom_field_options ) ) {
					asort( $custom_field_options );
					$options += $custom_field_options;
				}
				$options = array( '' => __( '— Select —', 'directories-pro' ) ) + $options;

				$form = array(
					'#header'    => array(
						array(
							'level'   => 'info',
							'message' => $this->_application->H( __( 'Set up the associations between the CSV file columns and content fields.', 'directories-pro' ) ),
						),
					),
					'#options'   => $options,
					'#optgroups' => $optgroups,
					'header'     => array(
						'#type'   => 'markup',
						'#markup' => '<table class="' . DRTS_BS_PREFIX . 'table drts-csv-table"><thead><tr><th style="width:25%;">' . __( 'Column Header', 'directories-pro' ) . '</th>'
							. '<th style="width:35%;">' . __( 'Row 1', 'directories-pro' ) . '</th>'
							. '<th style="width:40%;">' . __( 'Select field', 'directories-pro' ) . '</th></tr></thead><tbody>',
					),
					'fields'     => array(
						'#tree'             => true,
						'#element_validate' => array(
							array( array( $this, '_validateMapFields' ), array( $context, $fields, $bundle ) ),
						),
						'#submit'           => array(
							9 => array( // weight
								function ( $form ) use ( $bundle, $parents ) {
									$this->_application->getPlatform()->setOption( 'csv_import_map_fields_' . $bundle->name, (array) $form->getValue( array_merge( $parents, array( 'fields' ) ) ) );
								},
							),
						),
					),
					'footer'     => array(
						'#type'   => 'markup',
						'#markup' => '</tbody></table>',
					),
				);

				$prev_map_fields      = $this->_application->getPlatform()->getOption( 'csv_import_map_fields_' . $bundle->name, array() );
				$invalid_column_names = array();
				foreach ( $csv_columns as $column_key => $column_name ) {
					$column_name = $this->_getCsvColumnName( $column_name );
					if ( ! preg_match( '#^[a-zA-Z0-9/_-]+$#', $column_name ) ) {
						$invalid_column_names[] = $column_name;
						continue;
					}

					if ( isset( $csv_row1[ $column_key ] ) ) {
						$column_value = $csv_row1[ $column_key ];
						if ( strlen( $column_value ) > 300 ) {
							$column_value = $this->_application->System_Text_summarize( $column_value, 300 );
						}
						if ( strlen( $column_value ) ) {
							$column_value = '<code>' . $this->_application->H( $column_value ) . '</code>';
						}
					} else {
						$column_value = '';
					}
					$default_value = null;
					if ( isset( $prev_map_fields[ $column_name ] )
						&& isset( $options[ $prev_map_fields[ $column_name ] ] )
					) {
						$default_value = $prev_map_fields[ $column_name ];
					} elseif ( isset( $options[ $column_name ] )
							&& $column_name !== $id_column_key // do not make id column selected by default
						) {

							$default_value = $column_name;
					}
					$form['fields'][ $column_name ] = array(
						'#prefix'        => '<tr><td>' . $this->_application->H( $column_name ) . '</td><td>' . $column_value . '</td><td>',
						'#suffix'        => '</td></tr>',
						'#type'          => 'select',
						'#options'       => $options,
						'#default_value' => $default_value,
						'#optgroups'     => $optgroups,
					);
				}
				// Add warning if importing existing CSV file
				if ( $values['upload_csv']['type'] === 'existing' ) {
					$form['#header'][] = array(
						'level'   => 'warning',
						'message' => __( 'Leave as-is if you are unsure.', 'directories-pro' ),
					);
				}
				// Show invalid column name error
				if ( ! empty( $invalid_column_names ) ) {
					$form['#header'][] = array(
						'level'   => 'danger',
						'message' => sprintf(
							__( 'Invalid CSV column names were found: %s', 'directories-pro' ),
							'<code>' . implode( '</code>, <code>', array_map( array( $this->_application, 'H' ), $invalid_column_names ) ) . '</code>'
						),
					);
				}

				return $form;

			case 'importer_settings':
				$form                    = array( 'importers' => array() );
				$bundle                  = $this->_getBundle( $context );
				$enclosure               = $this->_getCsvFile( $bundle, $values['upload_csv'] )->getCsvControl()[1];
				$mapped_fields           = $values['map_fields']['fields'];
				$fields                  = $this->_application->Entity_Field( $this->_getBundle( $context )->name );
				$importers_by_field_type = $this->_application->CSV_Importers( true );
				$parents[]               = 'importers';
				foreach ( $mapped_fields as $column_name => $mapped_field ) {
					if ( ! $_mapped_field = explode( '__', $mapped_field ) ) {
						continue;
					}

					$field_name = array_shift( $_mapped_field );
					$column     = implode( '__', $_mapped_field );
					if ( empty( $fields[ $field_name ] ) ) {
						continue;
					}

					$field         = $fields[ $field_name ];
					$importer_name = $importers_by_field_type[ $field->getFieldType() ];
					if ( ! $importer = $this->_application->CSV_Importers_impl( $importer_name, true ) ) {
						continue;
					}
					$info       = $importer->csvImporterInfo();
					$columns    = $importer->csvImporterColumns( $field );
					$_parents   = $parents;
					$_parents[] = $field_name;
					if ( strlen( $column ) ) {
						$_parents[] = $column;
					}
					$default_settings = array();
					if ( ! empty( $info['default_settings'] ) && is_array( $info['default_settings'] ) ) {
						$default_settings += $info['default_settings'];
					}
					if ( $column_settings_form = $importer->csvImporterSettingsForm( $field, $default_settings, $column, $enclosure, $_parents ) ) {
						foreach ( array_keys( $column_settings_form ) as $key ) {
							if ( strpos( $key, '#' ) !== 0 ) {
								$column_settings_form[ $key ]['#horizontal'] = true;
							}
						}
						if ( strlen( $column ) ) {
							$form['importers'][ $field_name ][ $column ]                 = $column_settings_form;
							$form['importers'][ $field_name ][ $column ]['#title']       = $columns[ $column ];
							$form['importers'][ $field_name ][ $column ]['#collapsible'] = false;
						} else {
							$form['importers'][ $field_name ] = $column_settings_form;
						}
						$form['importers'][ $field_name ]['#collapsible'] = true;
						if ( ! isset( $form['importers'][ $field_name ]['#title'] ) ) {
							$form['importers'][ $field_name ]['#title'] = $this->_getFieldLabel( $field );
						}
					}
				}
				if ( empty( $form['importers'] ) ) {
					return array();
				}

				$form['importers']['#tree'] = true;
				$form['#header'][]          = array(
					'level'   => 'info',
					'message' => __( 'Please configure additional options for each field.', 'directories-pro' ),
				);

				// Add warning if importing existing CSV file
				if ( $values['upload_csv']['type'] === 'existing' ) {
					$form['#header'][] = array(
						'level'   => 'warning',
						'message' => __( 'Leave as-is if you are unsure.', 'directories-pro' ),
					);
				}
				return $this->_application->Filter( 'csv_import_settings_form', $form, array( $bundle, $values['upload_csv'] ) );

			case 'run':
				$bundle = $this->_getBundle( $context );
				$file   = $this->_getCsvFile( $bundle, $values['upload_csv'] );
				$file->seek( PHP_INT_MAX );
				$total = $file->key() - 1;
				$form  = array(
					'#header'       => array(
						array(
							'level'   => 'info',
							'message' => sprintf( __( '%d records will be imported.', 'directories-pro' ), $total ),
						),
					),
					'limit_request' => array(
						'#type'          => 'slider',
						'#title'         => __( 'Number of items to process per request', 'directories-pro' ),
						'#description'   => __( 'Adjust this setting if you are experiencing timeout errors.', 'directories-pro' ),
						'#default_value' => empty( $bundle->info['is_taxonomy'] ) ? 10 : 20,
						'#min_value'     => 1,
						'#max_value'     => 50,
						'#integer'       => true,
						'#horizontal'    => true,
					),
				);

				// Add option to select identifier field for update if ID field has not been mapped with any column
				$mapped_fields = $values['map_fields']['fields'];
				if ( ( $bundle->entitytype_name === 'post' && ! in_array( 'post_id', $mapped_fields ) )
					|| ( $bundle->entitytype_name === 'term' && ! in_array( 'term_id', $mapped_fields ) )
				) {
					$fields                  = $this->_application->Entity_Field( $this->_getBundle( $context )->name );
					$importers_by_field_type = $this->_application->CSV_Importers( true );
					$identifier_fields       = array();
					foreach ( $mapped_fields as $column_name => $mapped_field ) {
						if ( ! $_mapped_field = explode( '__', $mapped_field ) ) {
							continue;
						}

						$field_name = array_shift( $_mapped_field );
						$column     = implode( '__', $_mapped_field );
						if ( empty( $fields[ $field_name ] ) ) {
							continue;
						}

						$field         = $fields[ $field_name ];
						$importer_name = $importers_by_field_type[ $field->getFieldType() ];
						if ( ! $importer = $this->_application->CSV_Importers_impl( $importer_name, true ) ) {
							continue;
						}
						if ( in_array( $importer_name, array( 'entity_title', 'entity_slug', 'field_string', 'field_number' ) ) ) {
							$label = $this->_getFieldLabel( $field );
							if ( strlen( $column ) ) {
								$columns = $importer->csvImporterColumns( $field );
								$label  .= ' - ' . $columns[ $column ];
							}
							$identifier_fields[ $mapped_field . ',' . $importer_name ] = $label;
						}
					}
					if ( ! empty( $identifier_fields ) ) {
						$form['is_update']        = array(
							'#type'       => 'checkbox',
							'#title'      => __( 'Update existing items', 'directories-pro' ),
							'#horizontal' => true,
						);
						$form['identifier_field'] = array(
							'#type'         => 'select',
							'#horizontal'   => true,
							'#options'      => $identifier_fields,
							'#field_prefix' => __( 'Identifier field', 'directories-pro' ),
							'#states'       => array(
								'visible' => array(
									$is_update_selector = sprintf( '[name="%s"]', $this->_application->Form_Form_fieldName( array_merge( $parents, array( 'is_update' ) ) ) ) => array(
										'type'  => 'checked',
										'value' => true,
									),
								),
							),
						);
						$form['skip_not_found'] = array(
							'#type'          => 'checkbox',
							'#switch'        => false,
							'#horizontal'    => true,
							'#title'         => __( 'Skip if no matching item found', 'directories-pro' ),
							'#states'        => array(
								'visible' => array(
									$is_update_selector => array(
										'type'  => 'checked',
										'value' => true,
									),
								),
							),
							'#default_value' => true,
						);
					}
				}
				return $form;

			default:
		}
	}

	public function systemToolInit( array $settings, array &$storage, array &$logs, ?Context $context = null ) {
		define( 'DRTS_CSV_IMPORTING', true );

		$bundle = $this->_getBundle( $context );
		$file   = $this->_getCsvFile( $bundle, $settings['upload_csv'] );
		$file->seek( PHP_INT_MAX );
		$total = $file->key() - 1;
		return array( 'default' => $total );
	}

	public function systemToolRunTask( $task, array $settings, $iteration, $total, array &$storage, array &$logs, ?Context $context = null ) {
		$bundle = $this->_getBundle( $context );

		$csv_columns       = $this->_getCsvRows( $bundle, $settings['upload_csv'] );
		$csv_columns       = $csv_columns[0];
		$mapped_fields     = $settings['map_fields']['fields'];
		$importer_settings = (array) ( $settings['importer_settings']['importers'] ?? array() );

		$_mapped_fields          = array();
		$fields                  = $this->_application->Entity_Field( $bundle->name );
		$importers_by_field_type = $this->_application->CSV_Importers( true );
		$limit                   = $_limit = (int) $settings['run']['limit_request'];
		$offset                  = $limit * ( $iteration - 1 );
		$file                    = $this->_getCsvFile( $bundle, $settings['upload_csv'] );
		if ( ! isset( $storage['importer_storage'] ) ) {
			$storage['importer_storage'] = array();
		}
		$file->seek( $offset + 1 );
		while ( ! $file->eof() ) {
			$csv_row = $file->current();
			$file->next();
			if ( ! is_array( $csv_row )
				|| array( null ) === $csv_row // skip invalid/empty rows
			) {
				continue;
			}

			++$offset;

			$row_start = microtime( true );

			$logs['success'][] = sprintf( 'Starting CSV processing at row %d.', $offset );

			$values = array();
			foreach ( $csv_columns as $column_index => $column_name ) {
				$column_name = $this->_getCsvColumnName( $column_name );
				if ( ! isset( $csv_row[ $column_index ] )
					|| ! strlen( $csv_row[ $column_index ] )
				) {
					continue; // no valid value for this row column
				}

				if ( ! isset( $_mapped_fields[ $column_name ] ) ) {
					if ( ! isset( $mapped_fields[ $column_name ] )
						|| ( ! $_mapped_fields[ $column_name ] = explode( '__', $mapped_fields[ $column_name ] ) )
					) {
						// Unset column since mapped field is invalid, to stop further processing the column
						unset( $csv_columns[ $column_index ], $_mapped_fields[ $column_name ] );
						continue;
					}
				}

				// Check importer and field
				$field_name = $_mapped_fields[ $column_name ][0];
				if ( ( ! $field = @$fields[ $field_name ] )
					|| ( ! $importer_name = @$importers_by_field_type[ $field->getFieldType() ] )
					|| ( ! $importer = $this->_application->CSV_Importers_impl( $importer_name, true ) )
				) {
					// Unset column since mapped field is invalid, to stop further processing the column
					unset( $csv_columns[ $column_index ], $_mapped_fields[ $column_name ] );
					continue;
				}

				$column = implode( '__', array_slice( $_mapped_fields[ $column_name ], 1 ) );

				// Init importer settings
				if ( strlen( $column ) ) {
					$_importer_settings = $importer_settings[ $field_name ][ $column ] ?? array();
				} else {
					$_importer_settings = $importer_settings[ $field_name ] ?? array();
				}

				// Import
				try {
					$field_start       = microtime( true );
					$field_value       = $importer->csvImporterDoImport(
						$field,
						$_importer_settings,
						$column,
						$this->_application->Filter( 'csv_import_field_value', $csv_row[ $column_index ], array( $field, $_importer_settings, $column ) ),
						$storage['importer_storage'],
						$logs
					);
					$field_end         = microtime( true );
					$logs['success'][] = sprintf(
						'- Fetched value for importing field %s (Execution time: %s)',
						strlen( $column ) ? $field_name . ' - ' . $column : $field_name,
						number_format( ( $field_end - $field_start ) * 1000, 2 ) . ' ms'
					);
				} catch ( \Exception $e ) {
					$logs['error'][] = sprintf( '- Failed importing row #%d (Error: %s)', $offset, $e );
					continue 2; // abort importing the current row
				}

				if ( false === $field_value ) {
					// Unset column since mapped field is invalid, to stop further processing the column
					unset( $csv_columns[ $column_index ], $_mapped_fields[ $column_name ] );
					continue;
				}

				// Skip if no value to import
				if ( null === $field_value ) {
					continue;
				}

				if ( is_array( $field_value ) && isset( $values[ $field_name ] ) ) {
					foreach ( $field_value as $field_index => $_field_value ) {
						if ( null === $_field_value ) {
							continue;
						}

						if ( ! isset( $values[ $field_name ][ $field_index ] )
							|| ! is_array( $values[ $field_name ][ $field_index ] )
						) {
							$values[ $field_name ][ $field_index ] = $_field_value;
						} else {
							if ( strlen( $column )
								&& isset( $_field_value[ $column ] )
							) {
								// Always use value returned for own column
								$values[ $field_name ][ $field_index ][ $column ] = $_field_value[ $column ];
							}
							$values[ $field_name ][ $field_index ] += $_field_value;
						}
					}
				} else {
					$values[ $field_name ] = $field_value;
				}
			}

			$values = $this->_application->Filter( 'csv_import_field_values', $values, array( $bundle ) );

			try {
				if ( $entity = $this->_isUpdateEntity( $settings, $bundle, $values, $logs ) ) {
					$entity            = $this->_application->Entity_Save( $entity, $values );
					$logs['success'][] = sprintf(
						__( '- Updated: %1$s (ID: %2$d)', 'directories-pro' ),
						$this->_application->Entity_Title( $entity, true ),
						$entity->getId()
					);
				} else {
					if ( empty( $values['post_author'] ) ) {
						$values['post_author'] = 0;
					}
					$entity            = $this->_application->Entity_Save( $bundle, $values );
					$logs['success'][] = sprintf(
						__( '- Imported: %1$s (ID: %2$d)', 'directories-pro' ),
						$this->_application->Entity_Title( $entity, true ),
						$entity->getId()
					);
				}

				// Notify
				$this->_application->Action( 'csv_import_entity', array( $bundle, $entity, $values, $importer_settings ) );

				$row_end           = microtime( true );
				$logs['success'][] = sprintf(
					'CSV data on row number %d processed (Execution time: %s)',
					$offset,
					number_format( ( $row_end - $row_start ) * 1000, 2 ) . ' ms'
				);
			} catch ( \Exception $e ) {
				$logs['error'][] = sprintf( __( 'CSV data at row number %1$d could not be imported: %2$s', 'directories-pro' ), $offset, $e->getMessage() );
			}

			--$limit;
			if ( $limit === 0 ) {
				break;
			}
		}

		$file = null;

		if ( $_limit * $iteration >= $total ) {
			// This is the last iteration
			// Notify each importer that import has completed
			foreach ( array_keys( $_mapped_fields ) as $column_name ) {
				$field_name = $_mapped_fields[ $column_name ][0];
				$column     = implode( '__', array_slice( $_mapped_fields[ $column_name ], 1 ) );
				if ( ( ! $field = @$fields[ $field_name ] )
					|| ( ! $importer_name = @$importers_by_field_type[ $field->getFieldType() ] )
					|| ( ! $importer = $this->_application->CSV_Importers_impl( $importer_name, true ) )
				) {
					continue;
				}

				$importer->csvImporterOnComplete(
					$field,
					$importer_settings[ $field_name ][ $column_name ] ?? array(),
					$column,
					$storage['importer_storage'],
					$logs
				);
			}
		}

		return $_limit - $limit;
	}

	public function _validateExistingFile( Form $form, &$value, $element, $parents ) {
		if ( $form->getValue( array_merge( $parents, array( 'type' ) ) ) === 'existing' ) {
			try {
				new \SplFileObject( $form->getValue( array_merge( $parents, array( 'existing' ) ) ) );
			} catch ( \Exception $e ) {
				$form->setError( $e->getMessage(), $element );
			}
		}
	}

	public function _validateMapFields( $form, &$value, $element, $context, $fields, $bundle ) {
		$value           = array_filter( (array) $value );
		$required_fields = array();

		if ( ! empty( $bundle->info['parent'] ) ) {
			$required_fields[] = $bundle->entitytype_name . '_parent';
		}
		if ( ! empty( $bundle->info['parent_field'] ) ) {
			$required_fields[] = $bundle->info['parent_field'];
		}

		// Make sure required fields are going to be imported
		foreach ( $required_fields as $field_name ) {
			if ( isset( $fields[ $field_name ] ) && ! in_array( $this->_getFieldOptionValue( $field_name ), $value ) ) {
				$form->setError(
					sprintf(
						__( 'The following field needs to be selected: %s.', 'directories-pro' ),
						$this->_getFieldLabel( $fields[ $field_name ] )
					)
				);
			}
		}

		$count = array_count_values( $value );
		foreach ( $count as $option => $_count ) {
			if ( $option === '' || $_count <= 1 ) {
				continue;
			}

			$_option    = explode( '__', $option );
			$field_name = $_option[0];
			$form->setError(
				sprintf(
					__( 'You may not associate multiple columns with the field: %s', 'directories-pro' ),
					$this->_getFieldLabel( $fields[ $field_name ] )
				)
			);
		}
	}

	protected function _getFieldOptionValue( $fieldName, $column = '' ) {
		return strlen( $column ) ? $fieldName . '__' . $column : $fieldName;
	}

	protected function _getFieldLabel( IField $field ) {
		if ( ( $field_type = $this->_application->Field_Type( $field->getFieldType() ) )
			&& $field_type->fieldTypeInfo( 'admin_only' )
		) {
			$label = $field_type->fieldTypeInfo( 'label' );
		} else {
			$label = $field->getFieldLabel();
		}
		$label = $label . ' (' . $field->getFieldName() . ')';
		if ( $field->isCustomField() ) {
			$label = sprintf( __( 'Custom field - %s', 'directories-pro' ), $label );
		}
		return $label;
	}

	protected function _getCsvFile( Bundle $bundle, array $settings ) {
		if ( ! isset( $this->_csvFile ) ) {
			$delimiter      = empty( $settings['delimiter'] ) ? ',' : $settings['delimiter'];
			$enclosure      = empty( $settings['enclosure'] ) ? '"' : $settings['enclosure'];
			$file           = $settings['type'] === 'upload' ? $settings['upload']['saved_file_path'] : $settings['existing'];
			$this->_csvFile = new \SplFileObject( $file );
			$this->_csvFile->setFlags( \SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY );
			$this->_csvFile->setCsvControl( $delimiter, $enclosure );
		}

		return $this->_csvFile;
	}

	protected function _getCsvRows( Bundle $bundle, array $settings, $limit = 1 ) {
		$ret  = array();
		$file = $this->_getCsvFile( $bundle, $settings );
		for ( $file->rewind(); $file->key() < $limit; $file->next() ) {
			if ( $file->key() === 0 ) {
				$ret[] = preg_replace( '/^\xEF\xBB\xBF/', '', $file->current() ); // remove BOM
			} else {
				$ret[] = $file->current();
			}
		}
		return $ret;
	}

	protected function _getCsvColumnName( $columnName ) {
		return str_replace( ' ', '-', preg_replace( '/\s+/', ' ', trim( $columnName ) ) );
	}

	protected function _isUpdateEntity( array $settings, Bundle $bundle, array &$values, array &$logs ) {
		if ( in_array( $bundle->entitytype_name . '_id', $settings['map_fields']['fields'] ) ) {
			if ( ! empty( $values[ $bundle->entitytype_name . '_id' ] )
				&& ( $entity = $this->_application->Entity_Entity( $bundle->entitytype_name, $values[ $bundle->entitytype_name . '_id' ], false ) )
				&& $entity->getBundleName() === $bundle->name
			) {
				unset( $values[ $bundle->entitytype_name . '_id' ] );
				return $entity;
			}
		} else {
			$import_settings = $settings['run'];
			if ( ! empty( $import_settings['is_update'] )
				&& ! empty( $import_settings['identifier_field'] )
				&& ( $identifier_field_parts = explode( ',', $import_settings['identifier_field'] ) )
			) {
				$identifier_field = $identifier_field_parts[0];
				$value            = null;
				if ( ! empty( $values[ $identifier_field ] ) ) {
					$value = $values[ $identifier_field ];
					if ( is_array( $value ) ) {
						$value = array_values( $value )[0];
					}
					if ( is_array( $value )
						&& isset( $value['value'] )
					) {
						$value = $value['value'];
					}
					$field_name_to_query = $identifier_field;
					// Property fields need to be queried by property name
					if ( $identifier_field_parts[1] === 'entity_title' ) {
						$field_name_to_query = 'title';
					} elseif ( $identifier_field_parts[1] === 'entity_slug' ) {
						$field_name_to_query = 'slug';
					}
					if ( $entity = $this->_application->Entity_Query( $bundle->entitytype_name, $bundle->name )
						->fieldIs( $field_name_to_query, $value )
						->fetch( 1 )
					) {
						$entity         = array_shift( $entity );
						$logs['info'][] = sprintf(
							'Updating existing item (%s) found with value (%s) for field (%s).',
							$entity->getTitle(),
							empty( $value ) ? 'N/A' : $value,
							$identifier_field
						);
						unset( $values[ $identifier_field ] );
						return $entity;
					}
				}
				if ( empty( $import_settings['skip_not_found'] ) ) {
					throw new Exception\RuntimeException(
						sprintf(
							'Item with value (%s) for field (%s) not found for update.',
							empty( $value ) ? 'N/A' : $value,
							$identifier_field
						)
					);
				}
			}
		}
		return false;
	}
}
