<?php
/**
 * FIT TableEditor
 * 
 * $Id$
 * 
 * @author Daniel Jahnke <daniel.jahnke@web.de>
 * @package FIT
 * @license LGPL http://www.gnu.org/copyleft/lesser.html
 */
 
/**
 * FIT TableEditor
 * 
 * @version 0.4.2
 * @package FIT
 * @subpackage Parser
 */

/** 
 * path to user's fixtures
 */
if( !defined( 'PHPFIT_FIXTURE_DIR' ) ) {
    define( 'PHPFIT_FIXTURE_DIR', '.' );
}

/**
 * load Parser
 */
include_once 'PHPFIT/Parser.php';

/**
 * load Fixture
 */
include_once 'PHPFIT/Fixture.php';

class PHPFIT_TableEditor
{
   /**
    * parser object
    * @var object 
    */
    private $_parser;

   /**
    * file, contain the html-file
    * @var string
    */
    private $_file;

   /**
    * finalFile, file-string to save the modified table into original file
    * @var string
    */
    private $_finalFile;

   /**
    * action
    * @var string
    */
    private $_action;

   /**
    * tables, contain the tables of html-doc and cell-content
    * tables[ table_number ][ cell_number ]
    * @var array
    */
    private $_tables;

   /**
    * checkBox
    * @var array
    */
    private $_checkBox = array();

   /**
    * work file
    * file to edited
    * @var string
    */
    private $_workFile;

   /**
    * state
    * contain the state for the table method
    * tableEdit or tableSave or tableDelete
    * needed to display the button over the tables at function getHtml()
    * @var string
    */
    private $_state;

   /**
    * configuration array for table design edit/delete buttons
    *
    * self
    * - the to-be-called script
    * fixture
    * - kind of fixture
    * edit/delete/checkBox
    * - on | off ( if edit/delete-button should display )
    * cacheDir
    * - cache directory
    * dataDir
    * - source directory
    * css
    * - button      style of buttons
    * - inputField  style of input field
    * - column      style of append column
    */
    private $_config = array(
                        'self'              =>  null,
                        'edit'              => 'on',
                        'delete'            => 'on',
                        'checkBox'          => 'on',
                        'cacheDir'          => 'cache',
                        'dataDir'           => 'data',
                        'css-button'        => 'button',
                        'css-inputField'    => 'inputField',
                        'css-column'        => 'column',
                            );

   /**
    * row
    * row which will be edited
    * @var int 
    */
    private $_row;

   /**
    * constructor
    *
    * creates a parser and fixture object
    * decide wether function will be called
    * creates a copy form file and work with it
    * @param string $file the html file, that will be modified
    * @param array  $options contain the setting for tableEditor
    */
    public function __construct( $file, $options = null )
    {
        // create parser
        $this->_parser = new PHPFIT_Parser();

        if( $options != null ) {
            foreach( $options as $key => $value ) {
                $this->_config[$key] = $value;
            }
        }

        /*
        * creates a temp-file and work with it, if finalSave the temp-file-content
        * will be written into original file an unlink the temp file
        */
        $this->_finalFile = $this->_config['dataDir'] . $file;
        if( file_exists( $this->_config['cacheDir'] . '/' .  md5( $this->_finalFile ) ) ) {
            $this->_file = $this->_config['cacheDir'] . '/' . md5( $this->_finalFile );
        }
        else {
            $this->_file = $this->_config['dataDir'] . $file;
        }

        $content = file_get_contents( $this->_file );
        $this->_parser->parse( $content );
        $html = $this->_parser->serialize();

        $this->_workFile = $file;
    }

   /**
    * get fixture information
    * default fixture = columnfixture
    *
    * @param int $table     table number
    * @param bool $func     true retunrs the fixture functions, else return the fixture kind
    * @return               kind of fixture or return the fixture functions
    */
    private function _getFixtureInformation( $table, $dry = false  )
    {
        $includeFixtures = array(
                        'columnfixture' => 'Column',
                        'actionfixture' => 'Action',
                        'summary'       => 'Summary',
                        //'rowfixture' => 'Row.php',
                                );

        $fixtureName    = $this->_parser->getNodeValue( 'cData', $table, 0, 0 );
        $fixture        = PHPFIT_FIXTURE_DIR . '/' . str_replace( '.', '/', $fixtureName );

        $fix = '';
        try{
            $fix     = PHPFIT_Fixture::loadFixture( $fixtureName );
        }
        catch ( Exception $e ) {
            PHPFIT_ParserException::addMsg( 'no fixure found for this table! ' . $e->getMessage(), 10, $e->getFile(), $e->getLine() );
        }

        foreach( $includeFixtures as $key => $fixture ) {
            if( !include_once PHPFIT_FIXTURE_DIR . '/Fixture/' . $fixture . '.php' ) {
                PHPFIT_ParserException::addMsg( 'Could not load Fixture: PHPFIT_Fixture_' . $fixture, 2, __FILE__ , __LINE__ );
            }

            $fixStr = 'PHPFIT_Fixture_' . $fixture;

            if( $fix instanceof $fixStr  ) {
                return $key;
            }
        }
        //default columnfixture
        return 'columnfixture';
    }

   /**
    * edit row
    *
    * set to all cells in the editRow html-input-fields
    * @param int $table     table of the tables array
    * @param int $editRow   which row of selected table
    * @return bool true on success
    */
    public function editRow( $table, $editRow )
    {
        $row = $this->_parser->countChildNodes( $table );
        $col = $this->_parser->countChildNodes( $table, $row - 1 );

        $fixture = $this->_getFixtureInformation( $table );
        if( $fixture == 'columnfixture' ) {
            $startRow = 1;
        }
        if( $fixture == 'rowfixture' ) {
            $startRow = 1;
        }
        if( $fixture == 'actionfixture' ) {
            $startRow = 0;
        }

        for( $i = 0; $i < $row ; $i++ ) {
            if( $i == 0 || $i == $startRow || $i != $editRow) {
                continue;
            }

            for( $column = 0; $column < $col; $column++ ) {
                $value = $this->_parser->getNodeValue( 'cData', $table, $editRow, $column );
                if( $value == " " ) {
                    $value = '&nbsp;';
                }
                $value = '<input class="' . $this->_config['css-inputField'] . '" type="text" value="' . $value . '" name="table[' . $table . '][' . $editRow . '][' . $column . ']" />';
                if( $this->_parser->setNodeValue( 'cData', $value, $table, $editRow, $column ) == false ) {
                    return false;
                }
                $html = $this->_parser->serialize();
            }
        }
        return true;
    }

   /**
    * save row
    *
    * save modified row
    * @param int $table     table of the tables array
    * @param int saveRow    the modified row
    * @param array $cells   values of the row
    * @return string        modified table
    */
    public function saveRow( $table, $cells, $saveRow )
    {
        $row = $this->_parser->countChildNodes( $table );
        $col = $this->_parser->countChildNodes( $table, $row - 1 );

        for( $column = 0; $column < $col; $column++ ) {
               if( $cells[$saveRow][$column] == " " ) {
                $cells[$saveRow][$column] = '&nbsp;';
            }
            $this->_parser->setNodeValue( 'cData', $cells[$saveRow][$column], $table, $saveRow, $column );
        }
        return $this->_parser->serialize();
    }

   /**
    * save table
    *
    * if final = false
    * - the table will be saved at the cache - directory
    * if final = true
    * - the table will overwrite the original file at the data - directory
    * @param string $html   contain the html-table
    * @param bool $final    if true overwrite the original file, if false save to cache directory as md5
    */
    public function saveTable( $html, $final = false )
    {
        if( $final == true ) {
            if( file_put_contents( $this->_finalFile, $html ) ) {
                if( file_exists( $this->_config['cacheDir'] . '/' . md5( $this->_finalFile ) ) ) {
                    unlink( $this->_config['cacheDir'] . '/' . md5( $this->_finalFile ) );
                    return true;
                }
            }
        }
        if( file_put_contents( $this->_config['cacheDir'] . '/' . md5( $this->_finalFile ), $html ) ) {
            return true;
        }

        return false;
    }

   /**
    * display
    *
    * getHtml table with form-tags
    * and print it
    */
    public function getHtml()
    {
        $html= '';

        $summarySet = false;
        // prepare tables
        for( $table = 0; $table <  $this->_parser->countChildNodes(); $table++ ) {

            $fixture = $this->_getFixtureInformation( $table );
            if( ( $table+1 < $this->_parser->countChildNodes() ) && ( $this->_getFixtureInformation( $table+1 ) == 'summary' ) ) {
                $summarySet = true;
            }

            // do not create modify-button for summay table
            if( $fixture == 'summary' ) {
                continue;
            }

            $startRow = 0;
            if( $fixture == 'columnfixture' ) {
                $startRow = 2;
            }
            if( $fixture == 'rowfixture' ) {
                $startRow = 2;
            }
            if( $fixture == 'actionfixture' ) {
                $startRow = 1;
            }

            $rows = $this->_parser->countChildNodes( $table );

            $editCol    = $this->_parser->countChildNodes( $table, $rows - 1 );
            $delCol     = $editCol;
            $boxCol     = $editCol;

            // edit ON | OFF
            if( $this->_config['edit'] == 'on' && $this->_state != 'tableEdit' ) {

                $this->_parser->appendColumn( $table );

                // set the edit/save-button
                for( $i = $startRow; $i < $rows; $i++ ) {
                    if( $this->_row == $i ) {
                        $this->_parser->setNodeValue( 'cData', '<input type="submit" value="Save" " name="action[save_' . $table . '_' . $i . ']" > ', $table, $i, $editCol );
                        // css
                        $this->_parser->setNodeValue( 'attributes', array( 'class' => $this->_config['css-column']  ), $table, $i, $editCol );
                    }
                    else {
                        $this->_parser->setNodeValue( 'cData', '<input type="submit" value="Edit" name="action[edit_' . $table . '_' . $i . ']" > ', $table, $i, $editCol );
                        // css
                        $this->_parser->setNodeValue( 'attributes', array( 'class' => $this->_config['css-column']  ), $table, $i, $editCol );
                    }
                }
                $delCol += 1;
                ++$boxCol;
            }

            // delete ON | OFF
            if( $this->_config['delete'] == 'on' && $this->_state != 'tableEdit' ) {
                $this->_parser->appendColumn( $table );

                // set the delete-button
                for( $i = $startRow; $i < $rows; $i++ ) {
                    $this->_parser->setNodeValue( 'cData', '<input type="submit" value="Delete" name="action[delete_' . $table . '_' . $i . ']" >', $table, $i, $delCol );
                    // css
                    $this->_parser->setNodeValue( 'attributes', array( 'class' => $this->_config['css-column']  ), $table, $i, $delCol );
                }
                ++$boxCol;
            }

            // checkbox ON | OFF
            if( $this->_config['checkBox'] == 'on' ) {
                $this->_parser->appendColumn( $table );
                if( $this->_state == 'tableEdit' ) {
                    for( $k = 0; $k < $rows; $k++ ) {
                        if( in_array( $k, $this->_checkBox[0] ) ) {
                            $this->_parser->setNodeValue( 'cData', '<input type="checkbox" name="tableAction[checkBox_' . $table . '_' . $k . ']" checked="true" >' , $this->_checkBox[1], $k, $boxCol );
                            // css
                            $this->_parser->setNodeValue( 'attributes', array( 'class' => $this->_config['css-column']  ), $this->_checkBox[1], $k, $boxCol );
                        }
                    }
                }
                else {
                    // set the checkboxes
                    for( $i = $startRow; $i < $rows; $i++ ) {
                        $this->_parser->setNodeValue( 'cData', '<input type="checkbox" name="tableAction[checkBox_' . $table . '_' . $i . ']" >', $table, $i, $boxCol );
                        // css
                        $this->_parser->setNodeValue( 'attributes', array( 'class' => $this->_config['css-column']  ), $table, $i, $boxCol );
                    }
                }
            }

            $addRowButton = $this->_parser->getNodeValue( 'before', $table ) . "\n";

            // css 
            $addRowButton .= '<div class="' . $this->_config['css-button'] . '">';

            // append | remove summary table button 
/*
            if( $summarySet ) {
                $addRowButton .= '<input type="submit" value="Remove summary" name="action[tableEdit_' . $table . '_removeSummary]" >&nbsp';
                $summarySet = false;
            }
            else {
                $addRowButton .= '<input type="submit" value="Add summary" name="action[tableEdit_' . $table . '_addSummary]" >&nbsp';
            }
*/
            // do not show Append row-button if no option is set
            if( $this->_config['checkBox'] == 'off' && $this->_config['delete'] == 'off' && $this->_config['edit'] == 'off' ) {
                $addRowButton .= '&nbsp;';
            }
            else {
                // add Append row-button
                $addRowButton .= '<input type="submit" value="Append row" name="action[newRow_' . $table . '_append]" >&nbsp';
            }
            $addRowButton .= '<input type="hidden" value="' . $this->_workFile . '" name="file" >';

            if( $this->_config['checkBox'] == 'on' ) {
                // show Insert before row-button only if checkBox is on
                $addRowButton .= '<input type="submit" value="Insert before row" name="action[tableEdit_' . $table . '_newRow]" >&nbsp;&nbsp;&nbsp;&nbsp;';
                if( $this->_state == 'tableEdit' ) {
                    $addRowButton .= '<input type="submit" value="Save"   name="action[tableEdit_' . $table . '_save]" >&nbsp;';
                }
                else {
                    $addRowButton .= '<input type="submit" value="Edit"   name="action[tableEdit_' . $table . '_edit]" >&nbsp;';
                    $addRowButton .= '<input type="submit" value="Delete" name="action[tableEdit_' . $table . '_delete]" >&nbsp;';
                }
            }

            // css
            $addRowButton .= '</div>';

            $this->_parser->setNodeValue( 'before', $addRowButton, $table );

            $html = $this->_parser->serialize();
            $this->_stripHtml( &$html );
        }

        //$self = $_SERVER["PHP_SELF"] . '?file=' . $this->_workFile;
        $self   = $this->_config['self'] . '&amp;file=' . urlencode( $this->_workFile );
        
        // prepaire table
        $editor = array();
        array_push( $editor, '<form action="'. $self .'" method="post">' );
        array_push( $editor, $html );
        // css
        array_push( $editor, '<div class="' . $this->_config['css-button'] . '">' );
        array_push( $editor, '<input type="submit" value="Save Tables" name="action[saveFinal_' . 'write' . '_original]" >&nbsp;&nbsp;' );
        array_push( $editor, '<input type="button" value="Cancel" onclick="confirmation(\'' . $self . '\')">' );
        // css
        array_push( $editor, '</div>' );
        array_push( $editor, '</form>' );
        return $editor;
    }

   /**
    * strip Html
    * extract the body from html-string
    * @param string $html   html-string
    */
    private function _stripHtml( &$html )
    {
        // search array for available tags
        $stripArr   = array(
                        array( "start" => "<body", "end" => "</body>" ),
                        array( "start" => "<head", "end" => "</html>" ),
                        array( "start" => "<html", "end" => "</html>" ),
                           );

        $startStr = "";
        $endStr = "";
        // walk through the html-string and search after special html-tags
        foreach( $stripArr as $key => $tag ) {
            if( is_int( strpos( $html, $tag['start'] ) ) && is_int( strpos( $html, $tag['end'] ) ) ) {
                $startStr   = $tag['start'];
                $endStr     = $tag['end'];
                break;
            }
        }

        // if no special html-tags (see stripArr) were found, return
        if( empty( $startStr )  && empty( $endStr ) ) {
            return;
        }

        $start      = stripos( $html, $startStr );
        $end        = stripos( $html, $endStr );

        // ignore the attributes of opening special-tags
        if( is_int( $start ) && is_int( $end ) ) {
           while( $html[$start++] != '>' );
        }

        // cut the essetial part of html
        $html = mb_strcut( $html, $start, $end - $start );
    }

   /**
    * edit
    * call the editRow function
    * @param int $table table number
    * @param int $row   row number
    * @return bool              true on success
    */
    public function edit( $table, $row )
    {
        $this->editRow( $table, $row );
        // to set the edit-button  to save-button
        $this->_row = $row;
        return true;
    }

   /**
    * save
    * call the saveRow function and save the complete table to temp-file
    * @param int $table     table number
    * @param array $cells   cells content
    * @param int $row       row number
    * @return bool              true on success
    */
    public function save( $table, $cells, $row )
    {
        $html = $this->saveRow( $table, $cells, $row );
        $this->saveTable( $html );
        return true;
    }

   /**
    * saveFinal
    * call the saveTable function and set the final flag TRUE   
    * the original file will be overwritten!
    * @return bool              true on success
    */
    public function saveFinal()
    {
        $html = $this->_parser->serialize();
        $this->saveTable( $html, true );
        return true;
    }

   /**
    * delete
    * call the deleteRow function 
    * and save the table content to temp-file
    * @param int $table table number
    * @param int $row   row number
    * @return bool              true on success
    */
    public function delete( $table, $row )
    {
        $this->_parser->deleteRow( $table, $row );

        $html = $this->_parser->serialize();
        $this->saveTable( $html );
        return true;
    }

   /**
    * newRow
    * call the function appendRow from parser
    * and save the table content to temp-file
    * @param int $table         table number
    * @return bool              true on success
    */
    public function newRow( $table )
    {
        $this->_parser->appendRow( $table );
        $html = $this->_parser->serialize();
        $this->saveTable( $html );

        $row  = $this->_parser->countChildNodes( $table ) - 1;
        $this->edit( $table, $row );
        return true;
    }

   /**
    * cancel
    * deletes the temp-file from cacheDir
    * @return bool              true on success, else false
    */
    public function cancel()
    {
        if( file_exists( $this->_config['cacheDir'] . '/' . md5( $this->_finalFile ) ) ) {
            if( unlink( $this->_config['cacheDir'] . '/' . md5( $this->_finalFile ) ) ) {
                return true;
            }
            return false;
        }
        return true;
    }

   /**
    * tableEdit
    * to edit the selected rows form $table
    * $this->_state is necessary to display the correct button to edit the table
    * checkBoxes-array contains all rows which will be edited
    * @param int $table         tablenumber
    * @param array $checkBoxes  list of all rows which will be edited
    * @return bool              true on success
    */
    public function tableEdit( $table, $checkBoxes )
    {
        // if no rows are selected, do nothing
        if( empty( $checkBoxes ) ) {
            return true;
        }

        $this->_state = 'tableEdit';

        $box = array();
        foreach( $checkBoxes as $value ) {
            array_push( $box, $value[2] );
        }

        array_push( $this->_checkBox, $box );
        array_push( $this->_checkBox, $table );

        for( $i = 0; $i < count( $checkBoxes ); $i++ ) {
            $this->editRow( $table, $checkBoxes[$i][2] );
        }
        return true;
    }

   /**
    * tableDelete
    * to delete the selected rows form $table
    * $this->_state is necessary to display the correct button to delete the table
    * checkBoxes-array contains all rows which will be deleted
    * @param int $table         tablenumber
    * @param array $checkBoxes  list of all rows which will be edited
    * @return bool              true on success
    */
    public function tableDelete( $table, $checkBoxes )
    {
        $this->_state = 'tableDelete';

        $box = array();
        foreach( $checkBoxes as $value ) {
            array_push( $box, $value[2] );
        }

        $value = count( $checkBoxes ) - 1;
        for( $i = 0; $value >= $i; $value-- ) {
            // check deleting in correct table
            if( $checkBoxes[$value][1] == $table ) {
                $this->_parser->deleteRow( $table, $checkBoxes[$value][2] );
            }
        }
        $html = $this->_parser->serialize();

        $this->saveTable( $html );
        return true;
    }

   /**
    * table new Row
    * insert new row before the selected row 
    * checkBoxes-array contains all rows which will be deleted
    * @param int $table         tablenumber
    * @param array $checkBoxes  list of all rows which will be edited
    * @return bool              true on success
    * @todo after insert new row make rows editable
    */
    public function tableNewRow( $table, $checkBoxes )
    {
        $box = array();
        foreach( $checkBoxes as $value ) {
            array_push( $box, $value[2] );
        }

        $value = count( $checkBoxes ) - 1;
        for( $i = 0; $value >= $i; $value-- ) {
            // check deleting in correct table
            if( $checkBoxes[$value][1] == $table ) {
                $this->_parser->insertBeforeRow( $table, $checkBoxes[$value][2] );
            }
        }
        $html = $this->_parser->serialize();

        $this->saveTable( $html );
        return true;
    }

   /**
    * tableSave
    * to save the selected rows form $table
    * $this->_state is necessary to display the correct button to save the table
    * checkBoxes-array contains all rows which will be saved
    * @param int $table         tablenumber
    * @param array $checkBoxes  list of all rows which will be edited
    * @return bool              true on success
    */
    public function tableSave( $table, $checkBoxes, $cells )
    {
        $this->_state = 'tableSave';

        $saveRows = array();
        foreach( $checkBoxes as $value ) {
            array_push( $saveRows, $value[2] );
        }

        for( $i = 0; $i < count( $checkBoxes ); $i++ ) {
            $this->saveRow( $table, $cells[$table], $saveRows[$i] );
        }

        $html = $this->_parser->serialize();
        $this->saveTable( $html );

        return true;
    }

   /**
    * add summary table
    * to append fit.Summary table
    * @param int $table         tablenumber
    * @return bool              true on success
    */
    public function addSummaryTable( $table )
    {
        $content = '<br><table border="1"><tr><td>fit.Summary</td></tr></table>';
        $this->_parser->appendTable( $table, $content );

        $html = $this->_parser->serialize();
        $this->saveTable( $html );

        return true;
    }

   /**
    * remove summary table
    * remove a fit.Summary table
    * @param int $table         tablenumber
    * @return bool              true on success
    */
    public function removeSummaryTable( $table )
    {
        $this->_parser->removeTable( $table );

        $html = $this->_parser->serialize();
        $this->saveTable( $html );

        return true;
    }
}

?>