<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * contains a few csv file data access tools.
 *
 * PHP VERSION 5
 *
 * LICENSE: The MIT License
 *
 * Copyright (c) <2008> <Kazuyoshi Tlacaelel>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @category  File
 * @package   File_CSV_DataSource
 * @author    Kazuyoshi Tlacaelel <kazu.dev@gmail.com>
 * @copyright 2008 Kazuyoshi Tlacaelel
 * @license   The MIT License
 * @version   SVN: $Id: DataSource.php 824 2009-03-09 15:22:24Z ktlacaelel $
 * @link      http://code.google.com/p/php-csv-parser/
 */

/**
 * csv data fetcher
 *
 * Sample snippets refer to this csv file for demonstration.
 * <code>
 *   name,age,skill
 *   john,13,knows magic
 *   tanaka,8,makes sushi
 *   jose,5,dances salsa
 * </code>
 *
 * @category  File
 * @package   File_CSV_DataSource
 * @author    Kazuyoshi Tlacaelel <kazu.dev@gmail.com>
 * @copyright 2008 Kazuyoshi Tlacaelel
 * @license   The MIT License
 * @link      http://code.google.com/p/php-csv-parser/
 */
class File_CSV_DataSource
{
    public

    /**
     * csv parsing default-settings
     *
     * @var array
     * @access public
     */
    $settings = array(
        'delimiter' => ',',
        'eol' => ";",
        'length' => 999999,
        'escape' => '"'
    );

    protected

    /**
     * imported data from csv
     *
     * @var array
     * @access protected
     */
    $rows = array(),

    /**
     * csv file to parse
     *
     * @var string
     * @access protected
     */
    $_filename = '',

    /**
     * csv headers to parse
     *
     * @var array
     * @access protected
     */
    $headers = array();

    /**
     * data load initialize
     *
     * @param mixed $filename please look at the load() method
     *
     * @access public
     * @see load()
     * @return void
     */
    public function __construct($filename = null)
    {
        $this->load($filename);
    }

    /**
     * csv file loader
     *
     * indicates the object which file is to be loaded
     *
     * <code>
     *
     *   require_once 'File/CSV/DataSource.php';
     *
     *   $csv = new File_CSV_DataSource;
     *   $csv->load('my_cool.csv');
     *   var_export($csv->connect());
     *
     *   array (
     *     0 =>
     *     array (
     *       'name' => 'john',
     *       'age' => '13',
     *       'skill' => 'knows magic',
     *     ),
     *     1 =>
     *     array (
     *       'name' => 'tanaka',
     *       'age' => '8',
     *       'skill' => 'makes sushi',
     *     ),
     *     2 =>
     *     array (
     *       'name' => 'jose',
     *       'age' => '5',
     *       'skill' => 'dances salsa',
     *     ),
     *   )
     *
     * </code>
     *
     * @param string $filename the csv filename to load
     *
     * @access public
     * @return boolean true if file was loaded successfully
     * @see isSymmetric(), getAsymmetricRows(), symmetrize()
     */
    public function load($filename)
    {
        $this->_filename = $filename;
        $this->flush();
        return $this->parse();
    }

    /**
     * settings alterator
     *
     * lets you define different settings for scanning
     *
     * Given array will override the internal settings
     *
     * <code>
     *  $settings = array(
     *      'delimiter' => ',',
     *      'eol' => ";",
     *      'length' => 999999,
     *      'escape' => '"'
     *  );
     * </code>
     *
     * @param mixed $array containing settings to use
     *
     * @access public
     * @return boolean true if changes where applyed successfully
     * @see $settings
     */
    public function settings($array)
    {
        $this->settings = array_merge($this->settings, $array);
    }

    /**
     * header fetcher
     *
     * gets csv headers into an array
     *
     * <code>
     *
     *   var_export($csv->getHeaders());
     *
     *   array (
     *     0 => 'name',
     *     1 => 'age',
     *     2 => 'skill',
     *   )
     *
     * </code>
     *
     * @access public
     * @return array
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * header counter
     *
     * retrives the total number of loaded headers
     *
     * @access public
     * @return integer gets the length of headers
     */
    public function countHeaders()
    {
        return count($this->headers);
    }

    /**
     * header and row relationship builder
     *
     * Attempts to create a relationship for every single cell that
     * was captured and its corresponding header. The sample below shows
     * how a connection/relationship is built.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     *
     *  if (!$csv->isSymmetric()) {
     *      die('file has headers and rows with different lengths
     *      cannot connect');
     *  }
     *
     *  var_export($csv->connect());
     *
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     *
     * </code>
     *
     *
     * You can pass a collection of headers in an array to build
     * a connection for those columns only!
     *
     * <code>
     *
     *  var_export($csv->connect(array('age')));
     *
     *  array (
     *    0 =>
     *    array (
     *      'age' => '13',
     *    ),
     *    1 =>
     *    array (
     *      'age' => '8',
     *    ),
     *    2 =>
     *    array (
     *      'age' => '5',
     *    ),
     *  )
     *
     * </code>
     *
     * @param array $columns the columns to connect, if nothing
     * is given all headers will be used to create a connection
     *
     * @access public
     * @return array If the data is not symmetric an empty array
     * will be returned instead
     * @see isSymmetric(), getAsymmetricRows(), symmetrize(), getHeaders()
     */
    public function connect($columns = array())
    {
        if (!$this->isSymmetric()) {
            return array();
        }
        if (!is_array($columns)) {
            return array();
        }
        if ($columns === array()) {
            $columns = $this->headers;
        }

        $ret_arr = array();

        foreach ($this->rows as $record) {
            $item_array = array();
            foreach ($record as $column => $value) {
                $header = $this->headers[$column];
                if (in_array($header, $columns)) {
                    $item_array[$header] = $value;
                }
            }

            // do not append empty results
            if ($item_array !== array()) {
                array_push($ret_arr, $item_array);
            }
        }

        return $ret_arr;
    }

    /**
     * data length/symmetry checker
     *
     * tells if the headers and all of the contents length match.
     * Note: there is a lot of methods that won't work if data is not
     * symmetric this method is very important!
     *
     * @access public
     * @return boolean
     * @see symmetrize(), getAsymmetricRows(), isSymmetric()
     */
    public function isSymmetric()
    {
        $hc = count($this->headers);
        foreach ($this->rows as $row) {
            if (count($row) != $hc) {
                return false;
            }
        }
        return true;
    }

    /**
     * asymmetric data fetcher
     *
     * finds the rows that do not match the headers length
     *
     * lets assume that we add one more row to our csv file.
     * that has only two values. Something like
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     *   niki,6
     * </code>
     *
     * Then in our php code
     *
     * <code>
     *   $csv->load('my_cool.csv');
     *   var_export($csv->getAsymmetricRows());
     * </code>
     *
     * The result
     *
     * <code>
     *
     *   array (
     *     0 =>
     *     array (
     *       0 => 'niki',
     *       1 => '6',
     *     ),
     *   )
     *
     * </code>
     *
     * @access public
     * @return array filled with rows that do not match headers
     * @see getHeaders(), symmetrize(), isSymmetric(),
     * getAsymmetricRows()
     */
    public function getAsymmetricRows()
    {
        $ret_arr = array();
        $hc      = count($this->headers);
        foreach ($this->rows as $row) {
            if (count($row) != $hc) {
                $ret_arr[] = $row;
            }
        }
        return $ret_arr;
    }

    /**
     * all rows length equalizer
     *
     * makes the length of all rows and headers the same. If no $value is given
     * all unexistent cells will be filled with empty spaces
     *
     * @param mixed $value the value to fill the unexistent cells
     *
     * @access public
     * @return array
     * @see isSymmetric(), getAsymmetricRows(), symmetrize()
     */
    public function symmetrize($value = '')
    {
        $max_length = 0;

        foreach ($this->rows as $row) {
            $row_length = count($row);
            if ($max_length < $row_length) {
                $max_length = $row_length;
            }
        }

        if ($max_length == 0) {
            $max_length = count($this->headers);
        }

        foreach ($this->rows as $key => $row) {
            $this->rows[$key] = array_pad($row, $max_length, $value);
        }

        $this->headers = array_pad($this->headers, $max_length, $value);
    }

    /**
     * grid walker
     *
     * travels through the whole dataset executing a callback per each
     * cell
     *
     * Note: callback functions get the value of the cell as an
     * argument, and whatever that callback returns will be used to
     * replace the current value of that cell.
     *
     * @param string $callback the callback function to be called per
     * each cell in the dataset.
     *
     * @access public
     * @return void
     * @see walkColumn(), walkRow(), fillColumn(), fillRow(), fillCell()
     */
    public function walkGrid($callback)
    {
        foreach (array_keys($this->getRows()) as $key) {
            if (!$this->walkRow($key, $callback)) {
                return false;
            }
        }
        return true;
    }

    /**
     * column fetcher
     *
     * gets all the data for a specific column identified by $name
     *
     * Note $name is the same as the items returned by getHeaders()
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   $csv->load('my_cool.csv');
     *   var_export($csv->getColumn('name'));
     * </code>
     *
     * the above example outputs something like
     *
     * <code>
     *
     *   array (
     *     0 => 'john',
     *     1 => 'tanaka',
     *     2 => 'jose',
     *   )
     *
     * </code>
     *
     * @param string $name the name of the column to fetch
     *
     * @access public
     * @return array filled with values of a column
     * @see getHeaders(), fillColumn(), appendColumn(), getCell(), getRows(),
     * getRow(), hasColumn()
     */
    public function getColumn($name)
    {
        if (!in_array($name, $this->headers)) {
            return array();
        }
        $ret_arr = array();
        $key     = array_search($name, $this->headers, true);
        foreach ($this->rows as $data) {
            $ret_arr[] = $data[$key];
        }
        return $ret_arr;
    }

    /**
     * column existance checker
     *
     * checks if a column exists, columns are identified by their
     * header name.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   $csv->load('my_cool.csv');
     *   $headers = $csv->getHeaders();
     * </code>
     *
     * now lets check if the columns exist
     *
     * <code>
     *   var_export($csv->hasColumn($headers[0]));    // true
     *   var_export($csv->hasColumn('age'));          // true
     *   var_export($csv->hasColumn('I dont exist')); // false
     * </code>
     *
     * @param string $string an item returned by getHeaders()
     *
     * @access public
     * @return boolean
     * @see getHeaders()
     */
    public function hasColumn($string)
    {
        return in_array($string, $this->headers);
    }

    /**
     * column appender
     *
     * Appends a column and each or all values in it can be
     * dinamically filled. Only when the $values argument is given.
     * <code>
     *
     *
     *  var_export($csv->fillColumn('age', 99));
     *  true
     *
     *  var_export($csv->appendColumn('candy_ownership', array(99, 44, 65)));
     *  true
     *
     *  var_export($csv->appendColumn('import_id', 111111111));
     *  true
     *
     *  var_export($csv->connect());
     *
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => 99,
     *      'skill' => 'knows magic',
     *      'candy_ownership' => 99,
     *      'import_id' => 111111111,
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => 99,
     *      'skill' => 'makes sushi',
     *      'candy_ownership' => 44,
     *      'import_id' => 111111111,
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => 99,
     *      'skill' => 'dances salsa',
     *      'candy_ownership' => 65,
     *      'import_id' => 111111111,
     *    ),
     *  )
     *
     * </code>
     *
     * @param string $column an item returned by getHeaders()
     * @param mixed  $values same as fillColumn()
     *
     * @access public
     * @return boolean
     * @see getHeaders(), fillColumn(), fillCell(), createHeaders(),
     * setHeaders()
     */
    public function appendColumn($column, $values = null)
    {
        if ($this->hasColumn($column)) {
            return false;
        }
        $this->headers[] = $column;
        $length          = $this->countHeaders();
        $rows            = array();

        foreach ($this->rows as $row) {
            $rows[] = array_pad($row, $length, '');
        }

        $this->rows = $rows;

        if ($values === null) {
            $values = '';
        }

        return $this->fillColumn($column, $values);
    }

    /**
     * collumn data injector
     *
     * fills alll the data in the given column with $values
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   $csv->load('my_cool.csv');
     *
     *   // if the csv file loads
     *   if ($csv->load('my_cool.csv')) {
     *
     *      // grab all data within the age column
     *      var_export($csv->getColumn('age'));
     *
     *      // rename all values in it with the number 99
     *      var_export($csv->fillColumn('age', 99));
     *
     *      // grab all data within the age column
     *      var_export($csv->getColumn('age'));
     *
     *      // rename each value in a column independently
     *      $data = array(1, 2, 3);
     *      $csv->fillColumn('age', $data);
     *
     *      var_export($csv->getColumn('age'));
     *   }
     * </code>
     *
     * standard output
     *
     * <code>
     *   array (
     *     0 => '13',
     *     1 => '8',
     *     2 => '5',
     *   )
     * </code>
     *
     * <code>
     *   true
     * </code>
     *
     * <code>
     *   array (
     *     0 => 99,
     *     1 => 99,
     *     2 => 99,
     *   )
     * </code>
     *
     * <code>
     *   array (
     *     0 => 1,
     *     1 => 2,
     *     2 => 3,
     *   )
     * </code>
     *
     * @param mixed $column the column identified by a string
     * @param mixed $values ither one of the following
     *  - (Number) will fill the whole column with the value of number
     *  - (String) will fill the whole column with the value of string
     *  - (Array) will fill the while column with the values of array
     *    the array gets ignored if it does not match the length of rows
     *
     * @access public
     * @return void
     */
    public function fillColumn($column, $values = null)
    {
        if (!$this->hasColumn($column)) {
            return false;
        }

        if ($values === null) {
            return false;
        }

        if (!$this->isSymmetric()) {
            return false;
        }

        $y = array_search($column, $this->headers);

        if (is_numeric($values) || is_string($values)) {
            foreach (range(0, $this->countRows() -1) as $x) {
                $this->fillCell($x, $y, $values);
            }
            return true;
        }

        if ($values === array()) {
            return false;
        }

        $length = $this->countRows();
        if (is_array($values) && $length == count($values)) {
            for ($x = 0; $x < $length; $x++) {
                $this->fillCell($x, $y, $values[$x]);
            }
            return true;
        }

        return false;
    }

    /**
     * column remover
     *
     * Completly removes a whole column identified by $name
     * Note: that this function will only work if data is symmetric.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load the library and csv file
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     * </code>
     *
     * lets dump currently loaded data
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * and now let's remove the second column
     *
     * <code>
     *  var_export($csv->removeColumn('age'));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     * </code>
     *
     * those changes made let's dump the data again and see what we got
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param string $name same as the ones returned by getHeaders();
     *
     * @access public
     * @return boolean
     * @see hasColumn(), getHeaders(), createHeaders(), setHeaders(),
     * isSymmetric(), getAsymmetricRows()
     */
    public function removeColumn($name)
    {
        if (!in_array($name, $this->headers)) {
            return false;
        }

        if (!$this->isSymmetric()) {
            return false;
        }

        $key = array_search($name, $this->headers);
        unset($this->headers[$key]);
        $this->resetKeys($this->headers);

        foreach ($this->rows as $target => $row) {
            unset($this->rows[$target][$key]);
            $this->resetKeys($this->rows[$target]);
        }

        return $this->isSymmetric();
    }

    /**
     * column walker
     *
     * goes through the whole column and executes a callback for each
     * one of the cells in it.
     *
     * Note: callback functions get the value of the cell as an
     * argument, and whatever that callback returns will be used to
     * replace the current value of that cell.
     *
     * @param string $name     the header name used to identify the column
     * @param string $callback the callback function to be called per
     * each cell value
     *
     * @access public
     * @return boolean
     * @see getHeaders(), fillColumn(), appendColumn()
     */
    public function walkColumn($name, $callback)
    {
        if (!$this->isSymmetric()) {
            return false;
        }

        if (!$this->hasColumn($name)) {
            return false;
        }

        if (!function_exists($callback)) {
            return false;
        }

        $column = $this->getColumn($name);
        foreach ($column as $key => $cell) {
            $column[$key] = $callback($cell);
        }
        return $this->fillColumn($name, $column);
    }

    /**
     * cell fetcher
     *
     * gets the value of a specific cell by given coordinates
     *
     * Note: That indexes start with zero, and headers are not
     * searched!
     *
     * For example if we are trying to grab the cell that is in the
     * second row and the third column
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * we would do something like
     * <code>
     *   var_export($csv->getCell(1, 2));
     * </code>
     *
     * and get the following results
     * <code>
     *   'makes sushi'
     * </code>
     *
     * @param integer $x the row to fetch
     * @param integer $y the column to fetch
     *
     * @access public
     * @return mixed|false the value of the cell or false if the cell does
     * not exist
     * @see getHeaders(), hasCell(), getRow(), getRows(), getColumn()
     */
    public function getCell($x, $y)
    {
        if ($this->hasCell($x, $y)) {
            $row = $this->getRow($x);
            return $row[$y];
        }
        return false;
    }

    /**
     * cell value filler
     *
     * replaces the value of a specific cell
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *
     *   $csv = new File_CSV_DataSource;
     *
     *   // load the csv file
     *   $csv->load('my_cool.csv');
     *
     *   // find out if the given coordinate is valid
     *   if($csv->hasCell(1, 1)) {
     *
     *       // if so grab that cell and dump it
     *       var_export($csv->getCell(1, 1));       // '8'
     *
     *       // replace the value of that cell
     *       $csv->fillCell(1, 1, 'new value');  // true
     *
     *       // output the new value of the cell
     *       var_export($csv->getCell(1, 1));       // 'new value'
     *
     *   }
     * </code>
     *
     * now lets try to grab the whole row
     *
     * <code>
     *   // show the whole row
     *   var_export($csv->getRow(1));
     * </code>
     *
     * standard output
     *
     * <code>
     *   array (
     *     0 => 'tanaka',
     *     1 => 'new value',
     *     2 => 'makes sushi',
     *   )
     * </code>
     *
     * @param integer $x     the row to fetch
     * @param integer $y     the column to fetch
     * @param mixed   $value the value to fill the cell with
     *
     * @access public
     * @return boolean
     * @see hasCell(), getRow(), getRows(), getColumn()
     */
    public function fillCell($x, $y, $value)
    {
        if (!$this->hasCell($x, $y)) {
            return false;
        }
        $row            = $this->getRow($x);
        $row[$y]        = $value;
        $this->rows[$x] = $row;
        return true;
    }

    /**
     * checks if a coordinate is valid
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load the csv file
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   var_export($csv->load('my_cool.csv'));   // true if file is
     *                                            // loaded
     * </code>
     *
     * find out if a coordinate is valid
     *
     * <code>
     *   var_export($csv->hasCell(99, 3)); // false
     * </code>
     *
     * check again for a know valid coordinate and grab that cell
     *
     * <code>
     *   var_export($csv->hasCell(1, 1));  // true
     *   var_export($csv->getCell(1, 1));            // '8'
     * </code>
     *
     * @param mixed $x the row to fetch
     * @param mixed $y the column to fetch
     *
     * @access public
     * @return void
     */
    public function hasCell($x, $y)
    {
        $has_x = array_key_exists($x, $this->rows);
        $has_y = array_key_exists($y, $this->headers);
        return ($has_x && $has_y);
    }

    /**
     * row fetcher
     *
     * Note: first row is zero
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load the library and csv file
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     * </code>
     *
     * lets dump currently loaded data
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * Now let's fetch the second row
     *
     * <code>
     *  var_export($csv->getRow(1));
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 => 'tanaka',
     *    1 => '8',
     *    2 => 'makes sushi',
     *  )
     * </code>
     *
     * @param integer $number the row number to fetch
     *
     * @access public
     * @return array the row identified by number, if $number does
     * not exist an empty array is returned instead
     */
    public function getRow($number)
    {
        $raw = $this->rows;
        if (array_key_exists($number, $raw)) {
            return $raw[$number];
        }
        return array();
    }

    /**
     * multiple row fetcher
     *
     * Extracts a rows in the following fashion
     *   - all rows if no $range argument is given
     *   - a range of rows identified by their key
     *   - if rows in range are not found nothing is retrived instead
     *   - if no rows were found an empty array is returned
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load the library and csv file
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     * </code>
     *
     * lets dump currently loaded data
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now get the second and thirdh row
     *
     * <code>
     *  var_export($csv->getRows(array(1, 2)));
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      0 => 'tanaka',
     *      1 => '8',
     *      2 => 'makes sushi',
     *    ),
     *    1 =>
     *    array (
     *      0 => 'jose',
     *      1 => '5',
     *      2 => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now lets try something odd and the goodie third row
     *
     * <code>
     *  var_export($csv->getRows(array(9, 2)));
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      0 => 'jose',
     *      1 => '5',
     *      2 => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param array $range a list of rows to retrive
     *
     * @access public
     * @return array
     */
    public function getRows($range = array())
    {
        if (is_array($range) && ($range === array())) {
            return $this->rows;
        }

        if (!is_array($range)) {
            return $this->rows;
        }

        $ret_arr = array();
        foreach ($this->rows as $key => $row) {
            if (in_array($key, $range)) {
                $ret_arr[] = $row;
            }
        }
        return $ret_arr;
    }

    /**
     * row counter
     *
     * This function will exclude the headers
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * php implementation
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   $csv->load('my_cool.csv');
     *   var_export($csv->countRows()); // returns 3
     * </code>
     *
     * @access public
     * @return integer
     */
    public function countRows()
    {
        return count($this->rows);
    }

    /**
     * row appender
     *
     * Aggregates one more row to the currently loaded dataset
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     *
     * first let's load the file and output whatever was retrived.
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now lets do some modifications, let's try adding three rows.
     *
     * <code>
     *  var_export($csv->appendRow(1));
     *  var_export($csv->appendRow('2'));
     *  var_export($csv->appendRow(array(3, 3, 3)));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     *  true
     *  true
     * </code>
     *
     * and now let's try to see what has changed
     *
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *    3 =>
     *    array (
     *      'name' => 1,
     *      'age' => 1,
     *      'skill' => 1,
     *    ),
     *    4 =>
     *    array (
     *      'name' => '2',
     *      'age' => '2',
     *      'skill' => '2',
     *    ),
     *    5 =>
     *    array (
     *      'name' => 3,
     *      'age' => 3,
     *      'skill' => 3,
     *    ),
     *  )
     * </code>
     *
     * @param array $values the values to be appended to the row
     *
     * @access public
     * @return boolean
     */
    public function appendRow($values)
    {
        $this->rows[] = array();
        $this->symmetrize();
        return $this->fillRow($this->countRows() - 1, $values);
    }

    /**
     * fillRow
     *
     * Replaces the contents of cells in one given row with $values.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * if we load the csv file and fill the second row with new data?
     *
     * <code>
     *  // load the library
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *
     *  // load csv file
     *  $csv->load('my_cool.csv');
     *
     *  // fill exitent row
     *  var_export($csv->fillRow(1, 'x'));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     * </code>
     *
     * now let's dump whatever we have changed
     *
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'x',
     *      'age' => 'x',
     *      'skill' => 'x',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now lets try to fill the row with specific data for each cell
     *
     * <code>
     *  var_export($csv->fillRow(1, array(1, 2, 3)));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     * </code>
     *
     * and dump the results
     *
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 1,
     *      'age' => 2,
     *      'skill' => 3,
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param integer $row    the row to fill identified by its key
     * @param mixed   $values the value to use, if a string or number
     * is given the whole row will be replaced with this value.
     * if an array is given instead the values will be used to fill
     * the row. Only when the currently loaded dataset is symmetric
     *
     * @access public
     * @return boolean
     * @see isSymmetric(), getAsymmetricRows(), symmetrize(), fillColumn(),
     * fillCell(), appendRow()
     */
    public function fillRow($row, $values)
    {
        if (!$this->hasRow($row)) {
            return false;
        }

        if (is_string($values) || is_numeric($values)) {
            foreach ($this->rows[$row] as $key => $cell) {
                 $this->rows[$row][$key] = $values;
            }
            return true;
        }

        $eql_to_headers = ($this->countHeaders() == count($values));
        if (is_array($values) && $this->isSymmetric() && $eql_to_headers) {
            $this->rows[$row] = $values;
            return true;
        }

        return false;
    }

    /**
     * row existance checker
     *
     * Scans currently loaded dataset and
     * checks if a given row identified by $number exists
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load library and csv file
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     * </code>
     *
     * build a relationship and dump it so we can see the rows we will
     * be working with
     *
     * <code>
     *   var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>  // THIS ROW EXISTS!!!
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now lets check for row existance
     *
     * <code>
     *  var_export($csv->hasRow(1));
     *  var_export($csv->hasRow(-1));
     *  var_export($csv->hasRow(9999));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     *  false
     *  false
     * </code>
     *
     * @param mixed $number a numeric value that identifies the row
     * you are trying to fetch.
     *
     * @access public
     * @return boolean
     * @see getRow(), getRows(), appendRow(), fillRow()
     */
    public function hasRow($number)
    {
        return (in_array($number, array_keys($this->rows)));
    }

    /**
     * row remover
     *
     * removes one row from the current data set.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * first let's load the file and output whatever was retrived.
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * now lets remove the second row
     *
     * <code>
     *  var_export($csv->removeRow(1));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     * </code>
     *
     * now lets dump again the data and see what changes have been
     * made
     *
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param mixed $number the key that identifies that row
     *
     * @access public
     * @return boolean
     * @see hasColumn(), getHeaders(), createHeaders(), setHeaders(),
     * isSymmetric(), getAsymmetricRows()
     */
    public function removeRow($number)
    {
        $cnt = $this->countRows();
        $row = $this->getRow($number);
        if (is_array($row) && ($row != array())) {
            unset($this->rows[$number]);
        } else {
            return false;
        }
        $this->resetKeys($this->rows);
        return ($cnt == ($this->countRows() + 1));
    }

    /**
     * row walker
     *
     * goes through one full row of data and executes a callback
     * function per each cell in that row.
     *
     * Note: callback functions get the value of the cell as an
     * argument, and whatever that callback returns will be used to
     * replace the current value of that cell.
     *
     * @param string|integer $row      anything that is numeric is a valid row
     * identificator. As long as it is within the range of the currently
     * loaded dataset
     *
     * @param string         $callback the callback function to be executed
     * per each cell in a row
     *
     * @access public
     * @return boolean
     *  - false if callback does not exist
     *  - false if row does not exits
     */
    public function walkRow($row, $callback)
    {
        if (!function_exists($callback)) {
            return false;
        }
        if ($this->hasRow($row)) {
            foreach ($this->getRow($row) as $key => $value) {
                $this->rows[$row][$key] = $callback($value);
            }
            return true;
        }
        return false;
    }

    /**
     * raw data as array
     *
     * Gets the data that was retrived from the csv file as an array
     *
     * Note: that changes and alterations made to rows, columns and
     * values will also reflect on what this function retrives.
     *
     * @access public
     * @return array
     * @see connect(), getHeaders(), getRows(), isSymmetric(), getAsymmetricRows(),
     * symmetrize()
     */
    public function getRawArray()
    {
        $ret_arr   = array();
        $ret_arr[] = $this->headers;
        foreach ($this->rows as $row) {
            $ret_arr[] = $row;
        }
        return $ret_arr;
    }

    /**
     * header creator
     *
     * uses prefix and creates a header for each column suffixed by a
     * numeric value
     *
     * by default the first row is interpreted as headers but if we
     * have a csv file with data only and no headers it becomes really
     * annoying to work with the current loaded data.
     *
     * this function will create a set dinamically generated headers
     * and make the current headers accessable with the row handling
     * functions
     *
     * Note: that the csv file contains only data but no headers
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * checks if the csv file was loaded
     *
     * <code>
     *   $csv = new File_CSV_DataSource;
     *   if (!$csv->load('my_cool.csv')) {
     *      die('can not load csv file');
     *   }
     * </code>
     *
     * dump current headers
     *
     * <code>
     *   var_export($csv->getHeaders());
     * </code>
     *
     * standard output
     *
     * <code>
     *   array (
     *     0 => 'john',
     *     1 => '13',
     *     2 => 'knows magic',
     *   )
     * </code>
     *
     * generate headers named 'column' suffixed by a number and interpret
     * the previous headers as rows.
     *
     * <code>
     *   $csv->createHeaders('column')
     * </code>
     *
     * dump current headers
     *
     * <code>
     *   var_export($csv->getHeaders());
     * </code>
     *
     * standard output
     *
     * <code>
     *   array (
     *     0 => 'column_1',
     *     1 => 'column_2',
     *     2 => 'column_3',
     *   )
     * </code>
     *
     * build a relationship and dump it
     *
     * <code>
     *   var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *
     *  array (
     *    0 =>
     *    array (
     *      'column_1' => 'john',
     *      'column_2' => '13',
     *      'column_3' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'column_1' => 'tanaka',
     *      'column_2' => '8',
     *      'column_3' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'column_1' => 'jose',
     *      'column_2' => '5',
     *      'column_3' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param string $prefix string to use as prefix for each
     * independent header
     *
     * @access public
     * @return boolean fails if data is not symmetric
     * @see isSymmetric(), getAsymmetricRows()
     */
    public function createHeaders($prefix)
    {
        if (!$this->isSymmetric()) {
            return false;
        }

        $length = count($this->headers) + 1;
        $this->moveHeadersToRows();

        $ret_arr = array();
        for ($i = 1; $i < $length; $i ++) {
            $ret_arr[] = $prefix . "_$i";
        }
        $this->headers = $ret_arr;
        return $this->isSymmetric();
    }

    /**
     * header injector
     *
     * uses a $list of values which wil be used to replace current
     * headers.
     *
     * Note: that given $list must match the length of all rows.
     * known as symmetric. see isSymmetric() and getAsymmetricRows() methods
     *
     * Also, that current headers will be used as first row of data
     * and consecuently all rows order will change with this action.
     *
     * sample of a csv file "my_cool.csv"
     *
     * <code>
     *   name,age,skill
     *   john,13,knows magic
     *   tanaka,8,makes sushi
     *   jose,5,dances salsa
     * </code>
     *
     * load the library and csv file
     *
     * <code>
     *  require_once 'File/CSV/DataSource.php';
     *  $csv = new File_CSV_DataSource;
     *  $csv->load('my_cool.csv');
     * </code>
     *
     * lets dump currently loaded data
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'name' => 'john',
     *      'age' => '13',
     *      'skill' => 'knows magic',
     *    ),
     *    1 =>
     *    array (
     *      'name' => 'tanaka',
     *      'age' => '8',
     *      'skill' => 'makes sushi',
     *    ),
     *    2 =>
     *    array (
     *      'name' => 'jose',
     *      'age' => '5',
     *      'skill' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * And now lets create a new set of headers and attempt to inject
     * them into the current loaded dataset
     *
     * <code>
     *  $new_headers = array('a', 'b', 'c');
     *  var_export($csv->setHeaders($new_headers));
     * </code>
     *
     * output
     *
     * <code>
     *  true
     * </code>
     *
     * Now lets try the same with some headers that do not match the
     * current headers length. (this should fail)
     *
     * <code>
     *  $new_headers = array('a', 'b');
     *  var_export($csv->setHeaders($new_headers));
     * </code>
     *
     * output
     *
     * <code>
     *  false
     * </code>
     *
     * now let's dump whatever we have changed
     *
     * <code>
     *  var_export($csv->connect());
     * </code>
     *
     * output
     *
     * <code>
     *  array (
     *    0 =>
     *    array (
     *      'a' => 'name',
     *      'b' => 'age',
     *      'c' => 'skill',
     *    ),
     *    1 =>
     *    array (
     *      'a' => 'john',
     *      'b' => '13',
     *      'c' => 'knows magic',
     *    ),
     *    2 =>
     *    array (
     *      'a' => 'tanaka',
     *      'b' => '8',
     *      'c' => 'makes sushi',
     *    ),
     *    3 =>
     *    array (
     *      'a' => 'jose',
     *      'b' => '5',
     *      'c' => 'dances salsa',
     *    ),
     *  )
     * </code>
     *
     * @param array $list a collection of names to use as headers,
     *
     * @access public
     * @return boolean fails if data is not symmetric
     * @see isSymmetric(), getAsymmetricRows(), getHeaders(), createHeaders()
     */
    public function setHeaders($list)
    {
        if (!$this->isSymmetric()) {
            return false;
        }
        if (!is_array($list)) {
            return false;
        }
        if (count($list) != count($this->headers)) {
            return false;
        }
        $this->moveHeadersToRows();
        $this->headers = $list;
        return true;
    }

    /**
     * csv parser
     *
     * reads csv data and transforms it into php-data
     *
     * @access protected
     * @return boolean
     */
    protected function parse()
    {
        if (!$this->validates()) {
            return false;
        }

        $c = 0;
        $d = $this->settings['delimiter'];
        $e = $this->settings['escape'];
        $l = $this->settings['length'];

        $res = fopen($this->_filename, 'r');

        while ($keys = fgetcsv($res, $l, $d, $e)) {

            if ($c == 0) {
                $this->headers = $keys;
            } else {
                array_push($this->rows, $keys);
            }

            $c ++;
        }

        fclose($res);
        $this->removeEmpty();
        return true;
    }

    /**
     * empty row remover
     *
     * removes all records that have been defined but have no data.
     *
     * @access protected
     * @return array containing only the rows that have data
     */
    protected function removeEmpty()
    {
        $ret_arr = array();
        foreach ($this->rows as $row) {
            $line = trim(join('', $row));
            if (!empty($line)) {
                $ret_arr[] = $row;
            }
        }
        $this->rows = $ret_arr;
    }

    /**
     * csv file validator
     *
     * checks wheather if the given csv file is valid or not
     *
     * @access protected
     * @return boolean
     */
    protected function validates()
    {
        // file existance
        if (!file_exists($this->_filename)) {
            return false;
        }

        // file readability
        if (!is_readable($this->_filename)) {
            return false;
        }

        return true;
    }

    /**
     * header relocator
     *
     * @access protected
     * @return void
     */
    protected function moveHeadersToRows()
    {
        $arr   = array();
        $arr[] = $this->headers;
        foreach ($this->rows as $row) {
            $arr[] = $row;
        }
        $this->rows    = $arr;
        $this->headers = array();
    }

    /**
     * array key reseter
     *
     * makes sure that an array's keys are setted in a correct numerical order
     *
     * Note: that this function does not return anything, all changes
     * are made to the original array as a reference
     *
     * @param array &$array any array, if keys are strings they will
     * be replaced with numeric values
     *
     * @access protected
     * @return void
     */
    protected function resetKeys(&$array)
    {
        $arr = array();
        foreach ($array as $item) {
            $arr[] = $item;
        }
        $array = $arr;
    }

    /**
     * object data flusher
     *
     * tells this object to forget all data loaded and start from
     * scratch
     *
     * @access protected
     * @return void
     */
    protected function flush()
    {
        $this->rows    = array();
        $this->headers = array();
    }

}

?>
