<?php
/**
 * CodeIgniter report view
 *
 * @category CodeIgniter
 * @license /_licenses/untermStrich/agb_normal.pdf
 * @author Christian Koller
 * @copyright 2009 untermStrich
 * @package views
 * @version $Id$
*/

if (!defined('BASEPATH')) { exit('No direct script access allowed'); }

/**
 * A view for ods
 */
class View_fill_ods
{
    const FILE_MODE_CSV = 'csv';
    const FILE_MODE_XML = 'xml';
    
    /**
     * File prefix for ODS
     *
     * Keep lowercase!
     */
    const FILE_PREFIX_ODS = 'ods';
    const FILE_PREFIX_XML_CONTENT = 'xct';
    
    /**
     * The main CI object
     */
    protected $CI;
    
    private $file_mode = false;
    private $csv_file_info = false;
    private $xml_values = false;
    private $xml_values_rows = false;
    
    private $csv_handle = false;
    private $csv_header = false;
    private $csv_rows_count = false;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->CI = CI_Controller::get_instance();
    }
    
    /**
     * Is this process useable
     *
     * @param   integer $mode       One of the My_controller::m_... modes
     * @param   integer $file_size  File size
     * @return  boolean
     */
    public function is_useable($mode, $file_size)
    {
        return true; //For now
    }
    
    /**
     * Process the report based on the view mode
     *
     * @param   object  $viewinfo       The view info
     * @param   string  $file_mode      One of self::FILE_MODE_*
     * @param   string  $file_name      File name
     * @param   mixed   $csv_file_info  Eithe the file info for CSV or false
     * @return  mixed   Either void or the filename
     *
     * @see US_report_view_result::M_DIRECT
     * @see US_report_view_result::M_FILE
     */
    public function process(SimpleXMLElement $viewinfo, $file_mode, $file_name, $template_filename, $csv_file_info, bool $return_file_name=false)
    {
        $this->CI->load->model('Correspondence_types_model', 'correspondence_types', true);
        $this->CI->load->library('US_Ods', '', 'ods');
        $this->CI->load->helper('open_xhtml');
        $this->CI->load->helper('xml_errors');

        $this->file_mode = $file_mode;
        $this->csv_file_info = $csv_file_info;
        
        $file_name_ods = Report_config::get_report_tmp_name(self::FILE_PREFIX_ODS);
        $xmlonlypath = Report_config::get_report_tmp_name(self::FILE_PREFIX_XML_CONTENT);
        
        if (!@copy($template_filename, $file_name_ods)) {
            $msg = 'Failed to create temp file '.$file_name_ods.' from '.$template_filename;
            echo $msg; die();
        }
        
        $ok = $this->CI->ods->open_existing_zip($file_name_ods, $template_filename);
        if ($ok!==false) {
            basic_error($ok, null, __CLASS__.'::'.__FUNCTION__);
        }
        
        $content_xml = $this->CI->ods->getFromName('content.xml');
        if ($content_xml===false) {
            basic_error('Invalid ODT/ODS file "'.$tmppath.'" - Cannot open content.xml.', null, __CLASS__.'::'.__FUNCTION__);
        }
        file_put_contents($xmlonlypath, $content_xml);
        
        $this->_process_load_file($file_name);
        $xdoc = $this->_process_xml($xmlonlypath);
        $this->_process_close_file();
        
        $this->CI->ods->addFromString('content.xml', $this->save_xdox_to_string($xdoc));
        
        $this->CI->ods->close();
        
        if ($return_file_name===true) {
            return $file_name_ods;
        }

        $viewinfo->mime = 'application/vnd.oasis.opendocument.spreadsheet';
        $viewinfo->mime['ending'] = 'ods';
        
        Report::set_header($viewinfo);
        readfile($file_name_ods);
    }
    
    /**
     * Load the file for process
     * 
     * @param   string  $file_name          File name
     * @return  void
     */
    private function _process_load_file($file_name)
    {
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                if (($this->csv_handle = fopen($file_name, 'r')) === FALSE) {
                    basic_error('Cannot open file '.$file_name, null, __CLASS__.'::'.__FUNCTION__);
                }
                
                rewind($this->csv_handle);
                $this->csv_header = fgetcsv($this->csv_handle, 0, "\t", '"', '\\');
                $end_of_head_pos = ftell($this->csv_handle);

                $count = 0;
                while($a = fgets($this->csv_handle)) {
                    $count++;
                }
                $this->csv_rows_count = $count;
                
                fseek($this->csv_handle, $end_of_head_pos);
                
                break;
            case self::FILE_MODE_XML:
            default:
                $ignore_schema = null;
                $this->xml_values = $this->CI->xmlsimple->load($file_name, false, $ignore_schema, 'SimpleXMLIterator');
                $this->xml_values_rows = $this->xml_values->data->row;
                $this->xml_values_rows->rewind();
                break;
        }
    }
    
    /**
     * Close file
     */
    private function _process_close_file()
    {
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                fclose($this->csv_handle);
                break;
            case self::FILE_MODE_XML:
            default:
                break;
        }
    }

    /**
     * Get data row count
     * 
     * @return  integer
     */
    private function _process_get_rows_count()
    {
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                return $this->csv_rows_count;
            case self::FILE_MODE_XML:
            default:
                return $this->xml_values_rows->count();
        }
    }
    
    /**
     * Get possible header names
     * 
     * @return  array
     */
    private function _process_get_names()
    {
        $reply = array();
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                return $this->csv_header;
                break;
            case self::FILE_MODE_XML:
            default:
                foreach ($this->xml_values->head->columns->xpath('addi:field|field') as $item) {
                    array_push($reply, (string)$item['name']);
                }
                break;
        }
        return $reply;
    }
    
    /**
     * Get filter information
     * 
     * @return  string
     */
    private function _process_get_filters()
    {
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                return $this->_process_get_filters_prepare($this->csv_file_info['filter_infos']);
            case self::FILE_MODE_XML:
            default:
                return $this->_process_get_filters_prepare($this->xml_values->head->filters->filter);
        }
    }
    
    /**
     * Get filter info from arrays
     * 
     * @param   mixed   $filters        Filters list
     * @return  array
     */
    private function _process_get_filters_prepare($filters) 
    {
        $is_xml_mode = is_array($filters) ? false : true;
        
        $reply = array();
        $all_filters = '';
        foreach ($filters as $filter) {
            if ($is_xml_mode) {
                $filter_name = (string)$filter['name'];
                $filter_title = (string)$filter['title'];
                $filter_value = (string)$filter[0];
            } else {
                $filter_name = (string)$filter['id'];
                $filter_title = (string)$filter['title'];
                $filter_value = (string)$filter['value'];
            }

            $filter_title = html_entity_decode($filter_title);

            if (stripos($filter_name, 'print_')!==0) {
                if ($all_filters !== '') {
                    $all_filters .= "\n";
                }
                $all_filters .= $filter_title.': '.$filter_value;
            }

            $reply['report_filter__'.$filter_name] = $filter_value;
        }
        $reply['report_filter'] = $all_filters;
        
        return $reply;
    }
    
    /**
     * Get next row
     * 
     * @return  mixed   Either an array or false at end of file
     */
    private function _process_get_next_row()
    {
        switch ($this->file_mode) {
            case self::FILE_MODE_CSV:
                $reply = array();
                $row = fgetcsv($this->csv_handle, 0, "\t", '"', '\\');
                if ($row===null || $row===false) {
                    return false;
                }
                
                $item_count = count($this->csv_header);
                for ($index = 0; $index<$item_count; $index++) {
                    $field_name = $this->csv_header[$index];
                    
                    if (isset($this->csv_file_info['field_types'][$field_name])) {
                        $field_type = $this->csv_file_info['field_types'][$field_name];
                    } else {
                        $field_type = 'number';
                    }
                    
                    array_push(
                        $reply,
                        array(
                            'type'  => $field_type,
                            'name'  => $field_name,
                            0       => $row[$index],
                        )
                    );
                }
                
                return $reply;
            case self::FILE_MODE_XML:
            default:
                if (!$this->xml_values_rows->valid()) {
                    return false;
                }
                $row = $this->xml_values_rows->current();
                $this->xml_values_rows->next();
                return $row->xpath('addi:cell|cell');
        }
    }
    
    /**
     * Process the XML
     * 
     * @param   string  $content_xml_path   XML file path
     * @return  object
     */
    private function _process_xml($content_xml_path)
    {
        $this->CI->load->model('Currencies_model', 'currencies');
        $this->CI->load->helper('odsxlsx');
        
        list($xdoc, $xfinder, $xmlonlypath_ignore) = open_xhtml($content_xml_path, true);
        ods_clone_rows_to_fill_row_with_real_cells($xdoc, $xfinder);
        
        $values_rows = $this->_process_get_rows_count();

        if ($values_rows<=0) {
            return $xdoc;
        }
        
        $found_nodes = array();
        $found_page = null;
        $found_row = null;
        $found_col_max = 0;
        foreach ($this->_process_get_names() as $item_name) {
            $item_name = preg_replace( "/\s+/", "", $item_name);
            $xnodes = $xfinder->query('//table:named-range[@table:name="'.$item_name.'"]');
            if ($xnodes->length<=0) {
                continue;
            }
            
            $xitem = $xnodes->item(0);
            $cell_range = (string)$xitem->getAttribute('table:cell-range-address');
            if ($cell_range==='') {
                continue;
            }
            
            $cell_range_areas = explode(':', str_replace('$', '', $cell_range));
            $cell_range_parts = explode('.', str_replace('\'', '', $cell_range_areas[0]));

            if (count($cell_range_parts)<2) {
                basic_error('Invalid cell string "'.$cell_range.'". Page in cell-range-address missing.', null, __CLASS__.'::'.__FUNCTION__);
            }

            $cell_range_row_col = odsxlsx_explode_cell_number($cell_range_parts[1]);

            if ($found_page!==null) {
                if ($found_page!==$cell_range_parts[0]) {
                    basic_error('Invalid cell string "'.$cell_range.'". First defined page was "'.$found_page.'". Cannot mix pages for named cells in reports.', null, __CLASS__.'::'.__FUNCTION__);
                }
            }
            $found_page = str_replace('\'', '', $cell_range_parts[0]);

            if ($found_row!==null) {
                if ($found_row!==$cell_range_row_col['row']) {
                    basic_error('Invalid cell string "'.$cell_range.'". First defined row was "'.$found_row.'". Cannot mix rows for named cells in reports.', null, __CLASS__.'::'.__FUNCTION__);
                }
            }
            
            $found_row = $cell_range_row_col['row'];
            $found_end_of_reply_area = $found_row+$values_rows-1;

            $found_nodes[$item_name] = $cell_range_row_col['col_int'];

            if ($found_col_max<$cell_range_row_col['col_int']) {
                $found_col_max = $cell_range_row_col['col_int'];
            }
        }
        
        $xnode_items = $xfinder->query('//table:named-range');
        foreach ($xnode_items as $xcell) {
            $cell_range = (string)$xcell->getAttribute('table:cell-range-address');
            if ($cell_range==='') {
                continue;
            }
            
            $cell_range_areas = explode(':', str_replace('$', '', $cell_range));
            $cell_range_parts_first = explode('.', $cell_range_areas[0]);
            
            if (count($cell_range_parts_first)<2) {
                continue;
            }
            
            if ($found_page!==$cell_range_parts_first[0]) {
                continue;
            }
            
            $cell_range_row_col_first = odsxlsx_explode_cell_number($cell_range_parts_first[1]);
            
            
            if ($cell_range_row_col_first['row']===$found_row) {
                
                if (count($cell_range_areas)>=2) {
                    $cell_range_parts_2nd = explode('.', $cell_range_areas[1]);
                    $cell_range_row_col_2nd = odsxlsx_explode_cell_number($cell_range_parts_2nd[1]);
                    
                    if ($cell_range_row_col_first['row']==$cell_range_row_col_2nd['row']) {
                        $reply_area = '$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$found_row;
                        $reply_area .= ':$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$found_end_of_reply_area;
                        $xcell->setAttribute('table:cell-range-address', $reply_area);
                    }
                    
                } else {
                    $reply_area = '$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$found_row;
                    $reply_area .= ':$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$found_end_of_reply_area;
                    $xcell->setAttribute('table:cell-range-address', $reply_area);
                }
                
            } elseif ($cell_range_row_col_first['row']>$found_row) {
                
                if (count($cell_range_areas)>=2) {
                    $cell_range_parts_2nd = explode('.', $cell_range_areas[1]);
                    $cell_range_row_col_2nd = odsxlsx_explode_cell_number($cell_range_parts_2nd[1]);
                    
                    $reply_start_row = $cell_range_row_col_first['row']+$values_rows-1;
                    $reply_end_row = $cell_range_row_col_2nd['row']+$values_rows-1;
                    $reply_area = '$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$reply_start_row;
                    $reply_area .= ':$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$reply_end_row;                    
                } else {
                    $reply_area_row = $cell_range_row_col_first['row']+$values_rows-1;
                    $reply_area = '$'.$found_page.'.$'.$cell_range_row_col_first['col'].'$'.$reply_area_row;
                }
                
                $xcell->setAttribute('table:cell-range-address', $reply_area);
            }
            
        }
        
        $xnodes = $xfinder->query('//table:table[@table:name="'.$found_page.'"]/table:table-row['.$found_row.']');
        if ($xnodes->length!==1) {
            basic_error('Invalid file format. Cannot find row '.$found_row.' in table '.$found_page.'. [A]', null, __CLASS__.'::'.__FUNCTION__);
        }
        $xrow_in_use = $xnodes->item(0);
        
        ods_clone_cells_to_fill_row_with_real_cells($xrow_in_use, $found_col_max);
        
        $xrow_found_as_preset = $xrow_in_use->cloneNode(true);
        
        if ($xrow_in_use->nextSibling===null) {
            $xempty = $xdoc->createElement('table:table-row');
            $xempty->appendChild($xdoc->createElement('table:table-cell'));
            $xrow_in_use->parentNode->appendChild($xempty);
        }
        
        foreach ($this->_process_get_filters() as $f_key => $f_value) {
            $this->_set_string_value_to_named_cell($xdoc, $xfinder, $f_key, $f_value);
        }
        
        $curr_row = $found_row; $i = 0;
        while (true) {
            if ($i++>100000) {
                echo 'Too many rows'; die();
            }
            
            $row = $this->_process_get_next_row();
            if ($row===false) {
                break;
            }
            
            if ($curr_row!==$found_row) {
                $xrow_in_use = $xrow_in_use->parentNode->insertBefore($xrow_found_as_preset->cloneNode(true), $xrow_in_use->nextSibling);
            }
            
            $xnode_items = $xfinder->query('//table:table[@table:name="'.$found_page.'"]/table:table-row['.$curr_row.']/table:table-cell');
            foreach ($xnode_items as $xcell) {
                if ($xcell->hasAttribute('table:formula')) {
                    $formula = $xcell->getAttribute('table:formula');
                    $formula_mod = odsxlsx_formula_move_cell($formula, $found_row, $curr_row);
                    $xcell->setAttribute('table:formula', $formula_mod);
                }
            }
            
            foreach ($row as $cell) {
                $cell_name = (string)$cell['name'];
                
                if (isset($found_nodes[$cell_name])) {
                    $found_col = intval($found_nodes[$cell_name]);
                    
                    $xnodes = $xfinder->query('//table:table[@table:name="'.$found_page.'"]/table:table-row['.$curr_row.']/table:table-cell['.$found_col.']');
                    $xitem = $xnodes->item(0);
                    
                    $this->_set_value_to_node($xdoc, $xitem, $cell);
                }
            }
            
            $curr_row++;
            
        }
        
        $xnodes = $xfinder->query('//table:table[@table:name="'.$found_page.'"]/table:table-row[last()]');
        if ($xnodes->length!==1) {
            basic_error('Invalid file format. Cannot find last row.', null, __CLASS__.'::'.__FUNCTION__);
        }
        $xitem = $xnodes->item(0);
        if (intval($xitem->getAttribute('table:number-rows-repeated'))>10000) {
            $xitem->setAttribute('table:number-rows-repeated', 10000); //Set max row repeated value
        }
        
        
        $this->process_variables_special($xdoc, $xfinder);
        
        return $xdoc;
    }
    
    /**
     * Set the cell value of the defined cell name
     * 
     * @param   object      $xdoc           The DOMDocument
     * @param   object      $xfinder        The DomXPath
     * @param   string      $item_name      Cell name
     * @param   string      $item_value     Cell value
     * @return  void
     */
    private function _set_string_value_to_named_cell(DOMDocument $xdoc, DomXPath $xfinder, $item_name, $item_value)
    {
        $xnodes = $xfinder->query('//table:named-range[@table:name="'.$item_name.'"]');
        if ($xnodes->length<=0) {
            return;
        }

        $xitem = $xnodes->item(0);
        $cell_range = (string)$xitem->getAttribute('table:cell-range-address');
        if ($cell_range==='') {
            return;
        }
        
        $cell_range_areas = explode(':', str_replace('$', '', $cell_range));
        $cell_range_parts = explode('.', str_replace('\'', '', $cell_range_areas[0]));
        
        if (count($cell_range_parts)<2) {
            basic_error('Invalid cell string "'.$cell_range.'". Page in cell-range-address missing.', null, __CLASS__.'::'.__FUNCTION__);
        }

        $cell_range_row_col = odsxlsx_explode_cell_number($cell_range_parts[1]);
        
        
        $xnodes = $xfinder->query('//table:table[@table:name="'.$cell_range_parts[0].'"]/table:table-row['.$cell_range_row_col['row'].']');
        if ($xnodes->length!==1) {
            basic_error('Invalid file format. Cannot find row '.$cell_range_row_col['row'].' in table '.$cell_range_parts[0].'. [B]', null, __CLASS__.'::'.__FUNCTION__);
        }
        $xrow_in_use = $xnodes->item(0);
        
        ods_clone_cells_to_fill_row_with_real_cells($xrow_in_use, $cell_range_row_col['col_int']);
        
        $xnode_cols = $xfinder->query('table:table-cell['.$cell_range_row_col['col_int'].']', $xrow_in_use);
        $xitem = $xnode_cols->item(0);
        
        while ($xitem->hasChildNodes()) {
            $xitem->removeChild($xitem->firstChild);
        }
        
        if ($item_value!==null) {
            $elem = $xdoc->createElement('text:p');
            $elem->appendChild($xdoc->createTextNode($item_value));
            $xitem->appendchild($elem);
        }
    }

    
    /**
     * Set the value (from cell) to the Node (xitem)
     * 
     * @param   object      $xdoc       DOMDocument - Document
     * @param   object      $xitem      DOMNode - Item to add the info to
     * @param   object      $cell       SimpleXMLElement Cell item
     * @return  void
     */
    private function _set_value_to_node(DOMDocument $xdoc, DOMElement $xitem, $cell)
    {
        $value_type = null; 
        $value_name = null;
        $value = null;
        $value_p = null;
        $value_force_nothing = false;
        switch ((string)$cell['type']) {
            case 'number':
                $value_type = 'float';
                $value_name = 'value';
                $value = (float)$cell[0];
                break;
            case 'number2':
                $value_type = 'float';
                $value_name = 'value';
                $value = (float)$cell[0];
                break;
            case 'currency':
                $value_type = 'currency';
                $value_name = 'value';
                $value = (float)$cell[0];
                $value_p = (string)$cell['usnumber'].' '.$this->CI->currencies->get_company_currency_abbreviation();
                break;
            case 'bool':
                $value_type = 'boolean';
                $value_name = 'boolean-value';
                $value = (string)$cell[0];
                break;
            case 'date':
                $value_type = 'date';
                $value_name = 'date-value';
                $value = (string)$cell['xlsdate'];
                if ($value==='1900-01-01T00:00:00.000') {
                    $value = '';
                    $value_force_nothing = true;
                }
                break;
            case 'time':
                $value_type = 'time'; //That's a hack which works for OO and MSO
                $value_name = 'date-value';
                $value = (string)$cell['xlsdate'];
                if ($value==='1900-01-01T00:00:00.000') {
                    $value = '';
                    $value_force_nothing = true;
                }
                break;
            case 'timespan':
                $value_type = 'float';
                $value_name = 'value';
                $value = (float)$cell[0];
                break;
            default:
                $value_type = 'string';
                $value_p = (string)$cell[0];
                break;
        }
        
        if ($value_type!==null) {
            $xitem->setAttribute('office:value-type', $value_type);
            
            $attr = $xdoc->documentElement->getAttribute('xmlns:calcext');
            if (!empty($attr)) {
                $xitem->setAttribute('calcext:value-type', $value_type);
            }
        }
        
        if ($value_name!==null) {
            $xitem->setAttribute('office:'.$value_name, $value);
        }
        
        while ($xitem->hasChildNodes()) {
            $xitem->removeChild($xitem->firstChild);
        }
        
        if ($value_p!==null) {
            $elem = $xdoc->createElement('text:p');
            $elem->appendChild($xdoc->createTextNode($value_p));
            $xitem->appendchild($elem);
        }
        
        if ($value_force_nothing && $xitem->hasAttributes()) {
            $attributes = array();
            foreach ($xitem->attributes as $attr) {
                array_push($attributes, (string)$attr->nodeName);
            }
            
            foreach ($attributes as $attr) {
                if (stripos($attr, 'value-type')!==false) {
                    $xitem->removeAttribute($attr);
                } elseif (stripos($attr, '-value')!==false) {
                    $xitem->removeAttribute($attr);
                }
            }
        }
        
    }

    /**
     * Save the xdoc to file
     * 
     * @param   object      $xdoc           The DOMDocument
     * @return  string
     */
    protected function save_xdox_to_string(DOMDocument $xdoc)
    {
        $reply = $xdoc->saveXML();
        
        $errors = libxml_get_errors();
        if (!empty($errors)) {
            xml_error_basic_msg('Error saving ODX xml', $errors, __CLASS__.'::'.__FUNCTION__);
        }
        
        if ($reply===false) {
            basic_error('Error saving ODX xml', null, __CLASS__.'::'.__METHOD__);
        }
        
        unset($xdoc);
        libxml_clear_errors();
        
        return $reply;
    }

    /**
     * Update special, for type (ODT/ODS) - Remove values, so that the Office has to update the calculations
     * 
     * @param   object      $xdoc       The DOMDocument
     * @param   object      $xfinder    The DomXPath
     * @return  void
     */
    protected function process_variables_special(DOMDocument $xdoc, DomXPath $xfinder)
    {
        $xnodes = $xfinder->query('//table:table-cell[@table:formula]');
        
        if ($xnodes->length<1) {
            return;
        }
        
        foreach ($xnodes as $xnode) {
            $xnode->removeAttribute('office:value');
        }
    }
    
    
} // END class View_fill_ods
