<?php
/**
 * Use this to read CSV files.
 * PHP's fgetcsv() does not conform to RFC 4180.
 * In particular, it doesn't handle the correct quote escaping syntax.
 */
class ReadCSV {

	const field_start    = 0;
	const unquoted_field = 1;
	const quoted_field   = 2;
	const found_quote    = 3;
	const found_cr_q     = 4;
	const found_cr       = 5;
	var $is_file;
	private $file;
	private $file_handle;
	private $sep;
	// If $eof is TRUE, the next next_char() will return FALSE.
	// Note that this is different to feof(), which is TRUE
	// _after_ EOF is encountered.
	private $eof;
	private $nc;
	/**
	 * @param $file_handle
	 *  open file to read from
	 * @param $skip
	 *  initial character sequence to skip if found. e.g. UTF-8 byte-order mark
	 */
	public function __construct( $filename, $skip = "\xEF\xBB\xBF" ) {
		$this->is_file = false;
		if ( ! empty( $filename ) ) {
			$this->file  = $filename;
			$this->sep   = self::get_separator( $filename );
			$file_handle = @fopen( $filename, 'r' );
			if ( $file_handle ) {
				$this->is_file     = true;
				$this->file_handle = $file_handle;
				$this->nc          = fgetc( $this->file_handle );
				// skip junk at start
				for ( $i = 0; $i < strlen( $skip ); $i++ ) {
					if ( $this->nc !== $skip[ $i ] ) {
						break;
					}
					$this->nc = fgetc( $this->file_handle );
				}
				$this->eof = ( $this->nc === false );
			}
		}
	}
	public function closeFilePointer() {
		fclose( $this->file_handle );
	}
	public function get_data() {
		$first = true;
		$i     = 0;
		$data  = array();
		while ( ( $line = $this->get_single_row() ) !== null ) {
			// If the first line is empty, abort
			// If another line is empty, just skip it
			if ( empty( $line ) ) {
				if ( $first ) {
					break;
				} else {
					continue;
				}
			}
			// If we are on the first line, the columns are the headers
			if ( $first ) {
				$headers = $line;
				$first   = false;
				continue;
			}
			// Separate user data from meta
			foreach ( $line as $ckey => $column ) {
				$column_name                = $headers[ $ckey ];
				$column                     = trim( $column );
				$data[ $i ][ $column_name ] = $column;
			}
			$i++;
		}

		return $data;
	}
	public function get_single_row() {
		if ( $this->eof ) {
			return null;
		}

		$row   = array();
		$field = '';
		$state = self::field_start;

		while ( 1 ) {
			$char = $this->next_char();
			if ( $state == self::quoted_field ) {
				if ( $char === false ) {
					// EOF. (TODO: error case - no closing quote)
					$row[] = $field;
					return $row;
				}
				// Fall through to accumulate quoted chars in switch() {...}
			} elseif ( $char === false || $char == "\n" ) {
				// End of record.
				// (TODO: error case if $state==self::field_start here - trailing comma)
				$row[] = $field;
				return $row;
			} elseif ( $char == "\r" ) {
				// Possible start of \r\n line end, but might be just part of foo\rbar
				$state = ( $state == self::found_quote ) ? self::found_cr_q : self::found_cr;
				continue;
			} elseif ( $char == $this->sep &&
				( $state == self::field_start ||
				$state == self::found_quote ||
				$state == self::unquoted_field ) ) {
				// End of current field, start of next field
				$row[] = $field;
				$field = '';
				$state = self::field_start;
				continue;
			}

			switch ( $state ) {

				case self::field_start:
					if ( $char == '"' ) {
						$state = self::quoted_field;
					} else {
						$state  = self::unquoted_field;
						$field .= $char;
					}
					break;

				case self::quoted_field:
					if ( $char == '"' ) {
						$state = self::found_quote;
					} else {
						$field .= $char;
					}
					break;

				case self::unquoted_field:
					$field .= $char;
					// (TODO: error case if '"' in middle of unquoted field)
					break;

				case self::found_quote:
					// Found '"' escape sequence
					$field .= $char;
					$state  = self::quoted_field;
					// (TODO: error case if $char!='"' - non-separator char after single quote)
					break;

				case self::found_cr:
					// Lone \rX instead of \r\n. Treat as literal \rX. (TODO: error case?)
					$field .= "\r" . $char;
					$state  = self::unquoted_field;
					break;

				case self::found_cr_q:
					// (TODO: error case: "foo"\rX instead of "foo"\r\n or "foo"\n)
					$field .= "\r" . $char;
					$state  = self::quoted_field;
					break;
			}
		}
	}
	public function next_char() {
		$c         = $this->nc;
		$this->nc  = fgetc( $this->file_handle );
		$this->eof = ( $this->nc === false );
		return $c;
	}
	public function get_separator( $file ) {
		$file_detail = self::analyse_file( $file );
		$separator   = $file_detail['delimiter']['value'];
		return $separator;
	}
	public function analyse_file( $file, $capture_limit_in_kb = 100 ) {
		 // capture starting memory usage
		$output['peak_mem']['start'] = memory_get_peak_usage( true );
		// log the limit how much of the file was sampled (in Kb)
		$output['read_kb'] = $capture_limit_in_kb;
		// read in file
		$fh       = fopen( $file, 'r' );
		$contents = fread( $fh, ( $capture_limit_in_kb * 1024 ) ); // in KB
		fclose( $fh );
		// specify allowed field delimiters
		$delimiters = array(
			'comma'     => ',',
			'semicolon' => ';',
			'tab'       => "\t",
			'pipe'      => '|',
			'colon'     => ':',
		);
		// specify allowed line endings
		$line_endings = array(
			'rn' => "\r\n",
			'n'  => "\n",
			'r'  => "\r",
			'nr' => "\n\r",
		);
		// loop and count each line ending instance
		foreach ( $line_endings as $key => $value ) {
			$line_result[ $key ] = substr_count( $contents, $value );
		}
		// sort by largest array value
		asort( $line_result );
		// log to output array
		$output['line_ending']['results'] = $line_result;
		$output['line_ending']['count']   = end( $line_result );
		$output['line_ending']['key']     = key( $line_result );
		$output['line_ending']['value']   = $line_endings[ $output['line_ending']['key'] ];
		$lines                            = explode( $output['line_ending']['value'], $contents );
		// remove last line of array, as this maybe incomplete?
		array_pop( $lines );
		// create a string from the legal lines
		$complete_lines = implode( ' ', $lines );
		// log statistics to output array
		$output['lines']['count']  = count( $lines );
		$output['lines']['length'] = strlen( $complete_lines );
		// loop and count each delimiter instance
		foreach ( $delimiters as $delimiter_key => $delimiter ) {
			$delimiter_result[ $delimiter_key ] = substr_count( $complete_lines, $delimiter );
		}
		// sort by largest array value
		asort( $delimiter_result );
		// log statistics to output array with largest counts as the value
		$output['delimiter']['results'] = $delimiter_result;
		$output['delimiter']['count']   = end( $delimiter_result );
		$output['delimiter']['key']     = key( $delimiter_result );
		$output['delimiter']['value']   = $delimiters[ $output['delimiter']['key'] ];
		// capture ending memory usage
		$output['peak_mem']['end'] = memory_get_peak_usage( true );
		return $output;
	}
}
