<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
/*********************************************************************************
 * SugarCRM is a customer relationship management program developed by
 * SugarCRM, Inc. Copyright (C) 2004 - 2008 SugarCRM Inc.
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * 
 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
 * 
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU General Public License version 3.
 * 
 * In accordance with Section 7(b) of the GNU General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "Powered by
 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
 * technical reasons, the Appropriate Legal Notices must display the words
 * "Powered by SugarCRM".
 ********************************************************************************/
/*********************************************************************************
 * $Id$
 * Description:  Defines the English language pack for the base application.
 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
 * All Rights Reserved.
 * Contributor(s): ______________________________________..
 ********************************************************************************/
 
/**
 * This is the base class that all other SugarMerge objects extend 
 *
 */
class EditViewMerge{
	/**
	 * The variable name that is used with the file for example in editviewdefs and detailviewdefs it is $viewdefs
	 *
	 * @var STRING
	 */
	protected $varName = 'viewdefs';
	/**
	 * Enter the name of the parameter used in the $varName for example in editviewdefs and detailviewdefs it is 'EditView' and 'DetailView' respectively - $viewdefs['EditView']
	 *
	 * @var STRING
	 */
	protected $viewDefs = 'EditView';
	/**
	 * this will store the meta data for the original file
	 *
	 * @var ARRAY
	 */
	protected $originalData = array();
	/**
	 * this will store the meta data for the new file
	 *
	 * @var ARRAY
	 */
	protected $newData = array();
	/**
	 * this will store the meta data for the custom file
	 *
	 * @var ARRAY
	 */
	protected $customData = array();
	/**
	 * this will store an associative array contianing all the fields that are used in the original meta data file
	 *
	 * @var ARRAY
	 */
	protected $originalFields = array();
	/**
	 * this will store an associative array contianing all the fields that are used in the new meta data file
	 *
	 * @var ARRAY
	 */
	protected $newFields = array();
	/**
	 * this will store an associative array contianing all the fields that are used in the custom meta data file
	 *
	 * @var ARRAY
	 */
	protected $customFields = array();
	/**
	 * this will store an associative array contianing all the merged fields 
	 *
	 * @var ARRAY
	 */
	protected $mergedFields = array();
	/**
	 * the name of the module to be merged
	 *
	 * @var STRING
	 */
	protected $module = 'module';
	/**
	 * the max number of columns for this view
	 *
	 * @var INT
	 */
	protected $maxCols = 2;
	/**
	 * If we should use the best match algorithim
	 *
	 * @var BOOLEAN
	 */
	protected $bestMatch = true;
	/**
	 * The default panel we place the fields in if we aren't using the best match algorithim
	 *
	 * @var STRING
	 */
	protected $defaultPanel = 'DEFAULT';
	/**
	 * The name of the panels section in the meta data
	 *
	 * @var STRING
	 */
	protected $panelName = 'panels';
	
	/**
	 * The file pointer to log to if set to NULL it will use the GLOBALS['log'] if available and log to debug
	 *
	 * @var FILEPOINTER
	 */
	protected $fp = NULL;
	
	
	/**
	 * Determines if getFields should analyze panels to determine if it is a MultiPanel
	 *
	 * @var unknown_type
	 */
	protected $scanForMultiPanel = true;
	
	/**
	 * If true then it works as though it's a multipanel
	 *
	 * @var BOOLEAN
	 */
	protected $isMultiPanel = true;
	
	/**
	 * Clears out the values of the arrays so that the same object can be utilized
	 *
	 */
	protected function clear(){
		unset($this->newData);
		$this->newData = array();
		unset($this->customData);
	 	$this->customData = array();
	 	unset($this->originalData);
	 	$this->originalData = array();
	 	unset($this->newFields);
		$this->newFields = array();
		unset($this->customFields);
	 	$this->customFields = array();
	 	unset($this->originalFields);
	 	$this->originalFields = array();
	 	unset($this->mergedFields);
	 	$this->mergedFields = array();
	 	unset($this->mergeData);
	 	$this->mergeData = array();
	}
	
	/**
	 * Allows the user to choose to use the best match algorithim or not
	 *
	 * @param BOOLEAN $on
	 */
	public function setBestMatch($on=true){
		$this->bestMatch = $on;
	}
	
	
	/**
	 * Allows users to set the name to use as the default panel in the meta data
	 *
	 * @param STRING $name - name of the default panel
	 */
	public function setDefaultPanel($name = 'DEFAULT'){
		$this->defaultPanel = $name;
	}
	
	/**
	 * Allows the user to set a filepointer that is already open to log to
	 *
	 * @param FILEPOINTER $fp
	 */
	public function setLogFilePointer($fp){
		$this->fp = $fp;
	}
	
	/**
	 * opens the file with the 'a' parameter and use it to log messages to
	 *
	 * @param STRING $file - path to file we wish to log to
	 */
	public function setLogFile($file){
		$this->fp = fopen($file, 'a');
	}
	
	/**
	 * 
	 */
	/**
	 * returns true if $val1 and $val2 match otherwise it returns false
	 *
	 * @param MULTI $val1 - a value to compare to val2
	 * @param MULTI $val2 - a value to compare to val1
	 * @return BOOLEAN - if $val1 and $val2 match
	 */
	protected function areMatchingValues($val1, $val2){
		if(!is_array($val1)){
			//if val2 is an array and val1 isn't then it isn't a match
			if(is_array($val2)){
				return false;
			}
			//otherwise both are not arrays so we can return a comparison between them
			return $val1 == $val2;
		}else{
			//if val1 is an array and val2 isn't then it isn't a match
			if(!is_array($val2)){
				return false;
			}
		}
		foreach($val1 as $k=>$v){
			if(!isset($val2[$k]))return false;
			if(!$this->areMatchingValues($val1[$k], $val2[$k])){
				return false;
			}
			unset($val2[$k]);
			unset($val1[$k]);
		}
		//this implies that there are still values left  so the two must not match since we unset any matching values
		if(!empty($val2)){
			return false;
		}
		return true;
	}
	
	/**
	 * Recursiveley merges two arrays
	 *
	 * @param ARRAY $gimp - if keys match this arrays values are overriden 
	 * @param ARRAY $dom - if keys match this arrays values will override the others
	 * @return ARRAY $merged - the merges array
	 */
	function arrayMerge($gimp, $dom) {
	if(is_array($gimp) && is_array($dom)) {
		foreach($dom as $domKey => $domVal) {
			if(isset($gimp[$domKey])) {
				if(is_array($domVal)) {
					$gimp[$domKey] = $this->arrayMerge($gimp[$domKey], $dom[$domKey]);
				} else {
					$gimp[$domKey] = $domVal;
				}
			} else {
				$gimp[$domKey] = $domVal;
			}
		}
	}
	return $gimp;
}
	
	/**
	 * Merges the meta data of a single field
	 *
	 * @param ARRAY $orig - the original meta-data for this field
	 * @param ARRAY $new - the new meta-data for this field
	 * @param ARRAY $custom - the custom meta-data for this field
	 * @return ARRAY $merged - the merged meta-data
	 */
	protected function mergeField($orig, $new, $custom){
		$orig_custom = $this->areMatchingValues($orig, $custom);
		$new_custom = $this->areMatchingValues($new, $custom);
		// if both are true then there is nothing to merge since all three fields match
		if(!($orig_custom && $new_custom)){
			$this->log('merging field');
			$this->log('original meta-data');
			$this->log($orig);
			$this->log('new meta-data');
			$this->log($new);
			$this->log('custom meta-data');
			$this->log($custom);
			$this->log('merged meta-data');
			$log = true;
		}else{
			return $new;
		}
		//if orignal and custom match always take the new value or if new and custom match
		if($orig_custom || $new_custom){
			$this->log($new);
			return $new;
		}
		//if original and new match always take the custom
		if($this->areMatchingValues($orig, $new)){
			$this->log($custom);
			return $custom;
		}
		
		if(is_array($custom)) {
			//if both new and custom are arrays then at this point new != custom and orig != custom and orig != new  so let's merge the custom and the new and return that
			if(is_array($new)){
				$new = $this->arrayMerge($custom, $new);
				$this->log($new);
				return $new;
			}else{
				//otherwise we know that new is not an array and custom has been 'customized' so let's keep those customizations.
				$this->log($custom);
				return $custom;
			}
		}
		//default to returning the New version of the field 
		$this->log($new);
		return $new; 
	}
	
	/**
	 * Merges the fields together and stores them in $this->mergedFields
	 *
	 */
	protected function mergeFields(){
		
		
		foreach($this->customFields as $field=>$data){
			//if we have this field in both the new fields and the original fields - it has existed since the last install/upgrade
			if(isset($this->newFields[$field]) && isset($this->originalFields[$field])){
				//if both the custom field and the original match then we take the location of the new field since it hasn't moved
				if($this->areMatchingValues($this->customFields[$field]['loc'], $this->originalFields[$field]['loc'])
				//AND the custom field and new field location don't match 
				&& !$this->areMatchingValues($this->customFields[$field]['loc'], $this->newFields[$field]['loc'])){
						//we take the location of the new field
						$loc = $this->newFields[$field]['loc'];
						$loc['source'] = 'new';
				}else{
						//othewise we take the location of the custom field
						$loc = $this->customFields[$field]['loc'];
						$loc['source'] = 'custom';
				}
				//but we still merge the meta data of the three
				$this->mergedFields[$field] = array(
					'data'=>$this->mergeField($this->originalFields[$field]['data'], $this->newFields[$field]['data'], $this->customFields[$field]['data']), 
					'loc'=>$loc);
			//if it's not set in the new fields then it was a  custom field or an original field so we take the custom fields data and set the location source to custom
			}else if(!isset($this->newFields[$field])){
				if(isset($this->originalFields[$field])){
					//then it was removed by sugar
					unset($this->customFields[$field]);
					continue;
				}
				
				$this->mergedFields[$field] = $data;
				$this->mergedFields[$field]['loc']['source'] = 'custom';
			}else {
				
				//otherwise  the field is in both new and custom but not in the orignal so we merge the new and custom data together and take the location from the custom
				$this->mergedFields[$field] = array(
					'data'=>$this->mergeField('', $this->newFields[$field]['data'], $this->customFields[$field]['data']), 
					'loc'=>$this->customFields[$field]['loc']);
				
				$this->mergedFields[$field]['loc']['source'] = 'custom';
				
			}
			
		//then we clear out the field from 
		unset($this->originalFields[$field]);
		unset($this->customFields[$field]);
		unset($this->newFields[$field]);
		}
		
		/**
		 * These are fields that were removed by the customer
		 */
		
		foreach($this->originalFields as $field=>$data){
			unset($this->originalFields[$field]);
			unset($this->newFields[$field]);
		}
		
		/**
		 * These are fields that were added by sugar
		 */
		
		foreach($this->newFields as $field=>$data){
			$data['loc']['source']= 'new';
			$this->mergedFields[$field] = array(
					'data'=>$data['data'], 
					'loc'=>$data['loc']);
			$this->mergedFields[$field]['loc']['source'] = 'new';
			unset($this->newFields[$field]);
		}
	}
	
	/**
	 * Walks through the merged fields and places them in the appropriate place based on their location parameter as well as the choosen algorithim
	 *
	 * @return ARRAY $panels - the new panels section for the merged file
	 */
	protected function buildPanels(){
		$panels  = array();
		foreach($this->mergedFields as $field){
			//if no field is taking the location of the current field then let's take that location
			if(!isset($panels[$field['loc']['panel']][$field['loc']['row']][$field['loc']['col']])){
				$panels[$field['loc']['panel']][$field['loc']['row']][$field['loc']['col']] = $field['data'];
			}else{
				if($this->bestMatch){
					$row = $field['loc']['row'];
					//for best match as long as the column is filled let's keep walking down till we can fill it
					
					while(!empty($panels[$field['loc']['panel']][$row][$field['loc']['col']])){
						$row++;
					}
					//row should be at a point that there is no field in this location
					$panels[$field['loc']['panel']][$row][$field['loc']['col']] = $field['data'];
				}else{
					//so for not best match we place it in the default panel at the first available column for the row
					$row = 0;
					while(!empty($panels['default'][$row][$field['loc']['col']])){
						$row++;
					}
					$panels[$field['loc']['panel']][$row][$field['loc']['col']] = $field['data'];
				}
			}
			
		}
		foreach($panels as $k=>$panel){
			foreach($panel as $r=>$row){
					ksort($panels[$k][$r]);
			}
			ksort($panels[$k]);
		}
		return $panels;
	}
	
	/**
	 * Sets the panel section for the meta-data after it has been merged
	 *
	 */
	protected function setPanels(){
		$this->newData[$this->module][$this->viewDefs][$this->panelName] = $this->buildPanels();
		/*
		if(!$this->isMultiPanel) {
		   $this->newData[$this->module][$this->viewDefs][$this->panelName] = $this->newData[$this->module][$this->viewDefs][$this->panelName][$this->defaultPanel];
		}
        */
	}
	
	/**
	 * Parses out the fields for each files meta data and then calls on mergeFields and setPanels
	 *
	 */
	protected function mergeMetaData(){
		
		$this->originalFields = $this->getFields($this->originalData[$this->module][$this->viewDefs][$this->panelName]);
		$this->customFields = $this->getFields($this->customData[$this->module][$this->viewDefs][$this->panelName]);
		$this->newFields = $this->getFields($this->newData[$this->module][$this->viewDefs][$this->panelName]);
		$this->mergeFields();
		$this->setPanels();
		
	}
	/**
	 * This takes in a  list of panels and returns an associative array of field names to the meta-data of the field as well as the locations of that field
	 *
	 * @param ARRAY $panels - this is the 'panel' section of the meta-data
	 * @return ARRAY $fields - an associate array of fields and their meta-data as well as their location
	 */
	protected function getFields($panels){

		$fields = array();
		$blanks = 0;
        $setDefaultPanel = false;
        
		if(count($panels) == 1) {
		   $arrayKeys = array_keys($panels);
		   if(!empty($arrayKeys[0])) {
		      $panels = $panels[$arrayKeys[0]];
		   } else {
		   	  $panels = $panels[''];
		   }
		   $setDefaultPanel = true;   
		}		
		
		if($this->scanForMultiPanel){
			require_once('include/SugarFields/Parsers/MetaParser.php');			
			if($setDefaultPanel || !MetaParser::hasMultiplePanels($panels)) {
			   $panels = array($this->defaultPanel=>$panels);
			   $this->isMultiPanel = false;
			}
		}

		foreach($panels as $panel_id=>$panel){	
			foreach($panel as $row_id =>$rows){
				foreach($rows as $col_id=>$col){
					if(is_array($col)){
						if(isset($col['name'])) {
						   $field_name = $col['name'];
						}
					}else{
						$field_name = $col;
					}
					if(empty($field_name)){
							$field_name = 'BLANK_' . $blanks;
							$blanks++;
					}
					$fields[$field_name] = array('data'=>$col, 'loc'=>array('panel'=>$panel_id, 'row'=>$row_id, 'col'=>$col_id));
				}
			}		
		}
		return $fields;

	}
		
		
	
	
	
	/**
	 * Loads the meta data of the original, new, and custom file into the variables originalData, newData, and customData respectively
	 *
	 * @param STRING $module - name of the module's files that are to be merged
	 * @param STRING $original_file - path to the file that originally shipped with sugar
	 * @param STRING $new_file - path to the new file that is shipping with the patch 
	 * @param STRING $custom_file - path to the custom file
	 */
	protected function loadData($module, $original_file, $new_file, $custom_file){
		$this->module = $module;
		$varnmame = $this->varName;
		require($original_file);
		$this->originalData = $$varnmame;
		require($new_file);
		$this->newData = $$varnmame;
		if(file_exists($custom_file)){
			require($custom_file);
			$this->customData = $$varnmame;
		}else{
			$this->customData = $this->originalData;
		}
	}
	
	/**
	 * This will save the merged data to a file
	 *
	 * @param STRING $to - path of the file to save it to 
	 * @return BOOLEAN - success or failure of the save
	 */
	public function save($to){
		return write_array_to_file("viewdefs['$this->module']['$this->viewDefs']", $this->newData[$this->module][$this->viewDefs], $to);
	}
	
	/**
	 * This will return the meta data of the merged file
	 *
	 * @return ARRAY - the meta data of the merged file
	 */
	public function getData(){
		return $this->newData;
	}
	
	/**
	 * public function that will merge meta data from an original sugar file that shipped with the product, a customized file, and a new file shipped with an upgrade
	 *
	 * @param STRING $module - name of the module's files that are to be merged
	 * @param STRING $original_file - path to the file that originally shipped with sugar
	 * @param STRING $new_file - path to the new file that is shipping with the patch 
	 * @param STRING $custom_file - path to the custom file
	 * @param BOOLEAN $save - boolean on if it should save the results to the custom file or not
	 * @return BOOLEAN - if the merged file was saved if false is passed in for the save parameter it always returns true
	 */
	public function merge($module, $original_file, $new_file, $custom_file=false, $save=true){
		$this->clear();
		$this->log("\n\n". 'Starting a merge in ' . get_class($this));
		$this->log('merging the following files');
		$this->log('original file:'  . $original_file);
		$this->log('new file:'  . $new_file);
		$this->log('custom file:'  . $custom_file);
		if(empty($custom_file) && $save){
			return true;
		}else{
			$this->loadData($module, $original_file, $new_file, $custom_file);
			$this->mergeMetaData();
			if($save && !empty($this->newData) && !empty($custom_file)){
				//backup the file
				copy($custom_file, $custom_file . '.suback.php');
				return $this->save($custom_file);
			}
		}
		if(!$save)return true;
		return false;
	}
	/**
	 * Logs the given message if the message is not a string it will export it first. If $this->fp is NULL then it will try to log to the $GLOBALS['log'] if it is available
	 *
	 * @param MULTI $message
	 */
	protected  function log($message){
		if(!is_string($message)){
			$message = var_export($message, true);
		}
		if(!empty($this->fp)){
			fwrite($this->fp, $message. "\n");
		}else{
			if(!empty($GLOBALS['log'])){
				$GLOBALS['log']->debug($message . "\n");
			}
		}
		
	}
}

?>









