/* 
   Copyright (C) 2012, 2013, 2015, 2016, 2017, 2020 German A. Arias

   This file is part of FísicaLab application

   FísicaLab is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.
 
   This application 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
   Library General Public License for more details.
 
   You should have received a copy of the GNU General Public
   License along with this library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
*/

#import "FLSolver.h"

@interface FLSolver (Private)
- (BOOL) conversionsContainsKey: (NSString *)factor ofType: (NSString *)type;
- (NSNumber *) conversionFactorFor: (NSString *)factor ofType: (NSString *)type;
@end

@implementation FLSolver (Private)

- (BOOL) conversionsContainsKey: (NSString *)factor ofType: (NSString *)type
{
  if ([factor isEqualToString: _(@"degrees")])
    {
      return YES;
    }
  else if ([[conversions allKeys] containsObject: type])
    {
      if ([[[[conversions objectForKey: type] objectAtIndex: system] allKeys]
	    containsObject: factor])
	{
	  return YES;
	}
      else
	{
	  return NO;
	}
    }
  else
    {
      return NO;
    }
}

// Return the requested conversion factor.
- (NSNumber *) conversionFactorFor: (NSString *)factor ofType: (NSString *)type
{
  if ([factor isEqualToString: _(@"degrees")])
    {
      return [[[conversions objectForKey: type] objectAtIndex: system]
	       objectForKey: @"degrees"];
    }
  else
    {
      return [[[conversions objectForKey: type] objectAtIndex: system]
	       objectForKey: factor];
    }
}

@end

@implementation FLSolver

+ (NSArray *) dataForElementWithTag: (NSUInteger)tag forSystem: (NSUInteger)sys
{
  // Should be overridden.
  return nil;
}

- (id) init
{
  self = [super init];

  if (self)
    {
      messagesArray = nil;
      numbers = [NSCharacterSet characterSetWithCharactersInString:
				  @".-+0123456789E"];
      [numbers retain];
    }

  return self;
}

- (void) dealloc
{
  [messagesArray release];
  [numbers release];
  [super dealloc];
}

- (void) solveProblemWithData: (NSMutableDictionary *)list
{
  // Should be overridden.
}

// Accessor methods for units system.
- (void) setSystem: (NSUInteger)aNumber
{
  system = aNumber;
}

// Accessor methods for conversion factors.
- (void) setConversions: (NSDictionary *)aDictionary
{
  conversions = aDictionary;
}

// Accessor methods for text viewer which display the results.
- (void) setViewer: (id)aView
{
  viewer = aView;
}

// Set the name of the file with the messages for the module.
- (void) setMessagesFile: (NSString *)fileName
{
  NSArray *array;
  NSBundle *bundle;

  bundle = [NSBundle mainBundle];
  array = [NSArray arrayWithContentsOfFile:
		   [bundle pathForResource: fileName
				    ofType: @"plist"]];
  ASSIGN(messagesArray, array);
}

// Wrtie the message at viewer.
- (void) writeMessage: (NSString *)data
{
  [viewer writeMessage: data];
}

// Write the corresponding message at viewer.
- (void) writeMessageAtIndex: (NSUInteger)index
{
  [self writeMessageAtIndex: index withParameter: nil];
}

// Write the corresponding message at viewer with the provided parameter.
- (void) writeMessageAtIndex: (NSUInteger)index
               withParameter: (NSString *)aString
{
  if (aString == nil)
    {
      [viewer writeMessage: [messagesArray objectAtIndex: index]];
    }
  else
    {
      NSString *advice =
	[NSString stringWithFormat: [messagesArray objectAtIndex: index],
 		                    aString];

      [viewer writeMessage: advice];
    }
}

/*- (NSTextView *) viewer
{
  return viewer;
  }*/

// Check if string contains a conversion factor.
- (BOOL) hasConversionTheString: (NSString *)data
{
  NSCharacterSet *dataSet = [NSCharacterSet characterSetWithCharactersInString:
					      data];

  if ([dataSet characterIsMember: '@'])
    {
      return YES;
    }
  else
    {
      return NO;
    }
}

// Return the correspondig conversion factor.
- (double) conversionFactorFor: (NSString *)info
{
  NSArray *comps = [info componentsSeparatedByString: @"@"];

  return [[self conversionFactorFor: [comps objectAtIndex: 0]
			     ofType: [comps objectAtIndex: 1]] doubleValue];
}

// Check if string is a numerical data.
- (BOOL) isNumericDataTheString: (NSString *)data
{
  NSCharacterSet *dataSet = [NSCharacterSet characterSetWithCharactersInString:
					      data];

  if ([numbers isSupersetOfSet: dataSet] && ![data isEqualToString: @"E"])
    {
      return YES;
    }
  else
    {
      return NO;
    }
}

// Check if the name is not empty or equal to 0.
- (BOOL) isEmptyTheName: (NSString *)name
{
  if ([name isEqualToString: @"0"] ||
      ([name length] == 0) )
    {
      return YES;
    }
  else
    {
      return NO;
    }
}

/* Convert the string which represents a scientific notation to a
   numerical data. */
- (NSString *) scientificNotationFor: (double)aNumber
{
  int count = 0;
  double newValue = aNumber;
  NSString *value = nil;

  if ( (newValue > 0) && (newValue < 1) )
    {
      while (newValue < 1)
	{
	  newValue = newValue*10;
	  count++;
	}

      value = [NSString stringWithFormat: @"%.3fE-%d", newValue, count];
    }
  else if ( (newValue > -1) && (newValue < 0) )
    {
      while (newValue > -1)
	{
	  newValue = newValue*10;
	  count++;
	}

      value = [NSString stringWithFormat: @"%.3fE-%d", newValue, count];
    }
  else if (newValue > 1)
    {
      while (newValue >= 10)
	{
	  newValue = newValue/10;
	  count++;
	}
      
      value = [NSString stringWithFormat: @"%.3fE%d", newValue, count];
    }
  else if (newValue < -1)
    {
      while (newValue <= -10)
	{
	  newValue = newValue/10;
	  count++;
	}
      
      value = [NSString stringWithFormat: @"%.3fE%d", newValue, count];
    }
  
  return value;
}

// Reduce an angle to a value between 0 and 360.
- (void) reduceAngleAt: (NSUInteger)index in:(NSMutableArray *)anArray
{
  double ang;

  ang = [[anArray objectAtIndex: index] doubleValue];

  if ( ang >= 360 )
    {
      ang -= floor(ang/360)*360;
    }
  else if ( ang < 0 )
    {
      ang += (floor(-1*ang/360) + 1)*360;                
    }
  else
    {
      return;
    }

  [anArray replaceObjectAtIndex: index
		     withObject: [NSNumber numberWithDouble: ang]];
}

/* If the magnitude of the vector is negative change the sign and the angle.
 * Also reduce the angle to a value between 0 and 369.
 */
- (void) fixVectorMagnitudeAt: (NSUInteger)vIndex
                  withAngleAt: (NSUInteger)angIndex
                           in: (NSMutableArray *)anArray
{
  double f = 0, ang = 0;
  NSUInteger changeAngle = NO;

  if ([[anArray objectAtIndex: vIndex] doubleValue] < 0)
    {
      f = -1*[[anArray objectAtIndex: vIndex] doubleValue];
      [anArray replaceObjectAtIndex: vIndex
			 withObject: [NSNumber numberWithDouble: f]];
      changeAngle = YES;
    }

  ang = [[anArray objectAtIndex: angIndex] doubleValue];
		
  if (changeAngle)
    {
      ang += 180;
    }

  if ( ang >= 360 )
    {
      ang -= floor(ang/360)*360;
    }
  else if ( ang < 0 )
    {
      ang += (floor(-1*ang/360) + 1)*360;
    }
  else if (!changeAngle)
    {
      return;
    }

  [anArray replaceObjectAtIndex: angIndex
		     withObject: [NSNumber numberWithDouble: ang]];
}

// Search a better representation for a force vector.
- (void) fixForceMagnitudeAt: (NSUInteger)fIndex
                 withAngleAt: (NSUInteger)angIndex
                          in: (NSMutableArray *)anArray
{
  double f = 0, ang = 0;
  NSUInteger changeAngle = NO;
  
  ang = [[anArray objectAtIndex: angIndex] doubleValue];
		
  if ( ang >= 360 )
    {
      ang -= floor(ang/360)*360;
      changeAngle = YES;
    }
  else if ( ang < 0 )
    {
      ang += (floor(-1*ang/360) + 1)*360;
      changeAngle = YES;
    }

  if ( (ang >= 180) && (ang <= 270) )
    {
      ang -= 180;
      f = [[anArray objectAtIndex: fIndex] doubleValue];
      changeAngle = YES;

      [anArray replaceObjectAtIndex: fIndex
			 withObject: [NSNumber numberWithDouble: -1*f]];
    }

  if (changeAngle)
    {
      [anArray replaceObjectAtIndex: angIndex
			 withObject: [NSNumber numberWithDouble: ang]];
    }
}

// Apply the conversion factors and search the unknown data.
- (BOOL) searchUnknownsIn: (NSDictionary *)data
	       andStoreIn: (NSMutableArray *)unknowns
{
  BOOL error = NO;
  NSNumber *code;
  NSEnumerator *count;

  count = [[data allKeys] objectEnumerator];

  // Parse each element in the problem
  while ((code = [count nextObject]) && !error)
    {
      NSUInteger x;
      NSString *key = nil;
      NSArray *terms;
      NSNumber *number;
      

      NSMutableArray *info = [[data objectForKey: code] objectForKey: @"Data"];
      
      NSMutableArray *units = [[data objectForKey: code]
				objectForKey: @"Dimensions"];
      
      NSMutableArray *values = [[data objectForKey: code]
				 objectForKey: @"Values"];
      // Remove the values of previous calculations.
      [values removeAllObjects];

      // Parse the information of each element
      for (x = 0; x < [info count]; x++)
	{
	  NSString *data = [info objectAtIndex: x];
	  NSString *unit = [units objectAtIndex: x];

	  /* This is useful for thermodynamics module to skip some fields
	   * that are irrelevant in some cases.
	   */
	  if ([data isEqualToString: @"="])
	    {
	      continue;
	    }

	  // If the field don't contains a name, parse it.
	  if (![unit isEqualToString: @"nil"])
	    {
	      // If there is a conversion factor, apply this.
	      if ([self hasConversionTheString: data])
		{
		  terms = [data componentsSeparatedByString: @"@"];
		  key = [[terms objectAtIndex: 1] stringByTrimmingSpaces];

		  // Check if conversion factor is available.
		  if ([self conversionsContainsKey: key ofType: unit])
		    {
		      NSString *var = [[terms objectAtIndex: 0]
					stringByTrimmingSpaces];
		      
		      if ([self isNumericDataTheString: var])
			{
			  /* WARNING: Here we assume all the temperature data
			   * should be converted to Kelvin. Because currently
			   * all the modules related with heat uses only the
			   * SI system. But this can change in the future.
			   */
			  if (![key isEqualToString: @"C"] &&
			      ![key isEqualToString: @"R"] &&
			      ![key isEqualToString: @"F"])
			    {
			      number =
				[NSNumber numberWithDouble: [var doubleValue]*
				 [[self conversionFactorFor: key ofType: unit]
				   doubleValue]];
			    }
			  else if ([key isEqualToString: @"C"])
			    {
			      number = [NSNumber numberWithDouble:
						   [var doubleValue] + 273.15];
			    }
			  else if ([key isEqualToString: @"R"])
			    {
			      number = [NSNumber numberWithDouble:
			 		0.55555555*([var doubleValue] - 491.67)
					+ 273.15];
			    }
			  else
			    {
			      number = [NSNumber numberWithDouble:
					0.55555555*([var doubleValue] - 32)
					+ 273.15];
			    }
			  
			  [values addObject: [number stringValue]];
			}
		      else
			{
			  var = [var stringByAppendingString: @"@"];
			  var = [var stringByAppendingString: key];
			  var = [var stringByAppendingString: @"@"];
			  var = [var stringByAppendingString: unit];
			  
			  [values addObject: var];
			  if (![unknowns containsObject: var])
			    {
			      [unknowns addObject: var];
			    }
			}
		    }
		  else
		    {
		      error = YES;
		    }
		}
	      /* If there is a default conversion factor for this field,
	       * apply this.
	       */
	      else if ([self hasConversionTheString: unit])
		{
		  terms = [unit componentsSeparatedByString: @"@"];
		  key = [[terms objectAtIndex: 1] stringByTrimmingSpaces];

		  // Check if conversion factor is available.
		  if ([self conversionsContainsKey: key ofType: unit])
		    {
		      if ([self isNumericDataTheString: data])
			{
			  /* WARNING: Here we assume all the temperature data
			   * should be converted to Kelvin. Because currently
			   * all the modules related with heat uses only the
			   * SI system. But this can change in the future.
			   */
			  if (![key isEqualToString: @"C"] &&
			      ![key isEqualToString: @"R"] &&
			      ![key isEqualToString: @"F"])
			    {
			      number =
				[NSNumber numberWithDouble: [data doubleValue]*
				 [[self conversionFactorFor: key ofType: unit]
				   doubleValue]];
			    }
			  else if ([key isEqualToString: @"C"])
			    {
			      number = [NSNumber numberWithDouble:
						   [data doubleValue] + 273.15];
			    }
			  else if ([key isEqualToString: @"R"])
			    {
			      number = [NSNumber numberWithDouble:
					0.55555555*([data doubleValue] - 491.67)
				        + 273.15];
			    }
			  else
			    {
			      number = [NSNumber numberWithDouble:
				        0.55555555*([data doubleValue] - 32)
				        + 273.15];
			    }
			  
			  [values addObject: [number stringValue]];
			}
		      else
			{
			  data = [data stringByAppendingString: @"@"];
			  data = [data stringByAppendingString: key];
			  data = [data stringByAppendingString: @"@"];
			  data = [data stringByAppendingString: unit];
		  
			  [values addObject: data];
			  if (![unknowns containsObject: data])
			    {
			      [unknowns addObject: data];
			    }
			}
		    }
		  else
		    {
		      error = YES;
		    }
		}
	      else
		{
		  // If is a numerical data without a conversion factor,
		  // pass it.
		  if ([self isNumericDataTheString: data])
		    {
		      [values addObject: data];
		    }
		  else
		    {
		      /* Process a simple unknown data (without conversion
		       * factor). But add the corresponding unit, so FisicaLab
		       * display it on the results.
		       */
		      NSString *varFactor = nil;
		  
		      // Check the dimensions
		      if ([unit isEqualToString: @"\%"])
			{
			  varFactor = @"\%";
			}
		      else if ([unit isEqualToString: @"ad"])
			{
			  varFactor = @"ad";
			}
		      else if ([unit isEqualToString: @"degrees"])
			{
			  varFactor = _(@"degrees");
			}
		      else if ([unit isEqualToString: @"rad/T"])
			{
			  varFactor = @"rad/s";
			}
		      else if ([unit isEqualToString: @"rad/T2"])
			{
			  varFactor = @"rad/s2";
			}
		      else if ([unit isEqualToString: @"rev"])
			{
			  varFactor = @"rev";
			}
		      else if ([unit isEqualToString: @"1/T"])
			{
			  varFactor = @"hz";
			}
		      else if ([unit isEqualToString: @"T"])
			{
			  varFactor = @"s";
			}
		      else if ([unit isEqualToString: @"M"])
			{
			  if (system == 0)
			    {
			      varFactor = @"kg";
			    }
			  else
			    {
			      varFactor = @"slug";
			    }
			}
		      else if ([unit isEqualToString: @"M/L3"])
			{
			  if (system == 0)
			    {
			      varFactor = @"kg/m3";
			    }
			  else
			    {
			      varFactor = @"slug/ft3";
			    }
			}
		      else if ([unit isEqualToString: @"L"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m";
			    }
			  else
			    {
			      varFactor = @"ft";
			    }
			}
		      else if ([unit isEqualToString: @"L2"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m2";
			    }
			  else
			    {
			      varFactor = @"ft2";
			    }
			}
		      else if ([unit isEqualToString: @"L3"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m3";
			    }
			  else
			    {
			      varFactor = @"ft3";
			    }
			}
		      else if ([unit isEqualToString: @"L/T"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m/s";
			    }
			  else
			    {
			      varFactor = @"ft/s";
			    }
			}
		      else if ([unit isEqualToString: @"L/T2"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m/s2";
			    }
			  else
			    {
			      varFactor = @"ft/s2";
			    }
			}
		      else if ([unit isEqualToString: @"L3/T"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m3/s";
			    }
			  else
			    {
			      varFactor = @"ft3/s";
			    }
			}
		      else if ([unit isEqualToString: @"L3/M"])
			{
			  if (system == 0)
			    {
			      varFactor = @"m3/kg";
			    }
			  else
			    {
			      varFactor = @"ft3/slug";
			    }
			}
		      else if ([unit isEqualToString: @"M*L/T2"])
			{
			  if (system == 0)
			    {
			      varFactor = @"N";
			    }
			  else
			    {
			      varFactor = @"lb";
			    }
			}
		      else if ([unit isEqualToString: @"M/T2"])
			{
			  // Spring constant
			  if (system == 0)
			    {
			      varFactor = @"N/m";
			    }
			  else
			    {
			      varFactor = @"lb/ft";
			    }
			}
		      else if ([unit isEqualToString: @"L2*M/T2"])
			{
			  // Torque
			  if (system == 0)
			    {
			      varFactor = @"N*m";
			    }
			  else
			    {
			      varFactor = @"lb*ft";
			    }
			}
		      else if ([unit isEqualToString: @"M*L2/T2"])
			{
			  // Energy
			  if (system == 0)
			    {
			      varFactor = @"J";
			    }
			  else
			    {
			      varFactor = @"lb*ft";
			    }
			}
		      else if ([unit isEqualToString: @"M*L/T"])
			{
			  // Impulse
			  if (system == 0)
			    {
			      varFactor = @"N*s";
			    }
			  else
			    {
			      varFactor = @"lb*s";
			    }
			}
		      else if ([unit isEqualToString: @"M*L2/T3"])
			{
			  // Power
			  if (system == 0)
			    {
			      varFactor = @"W";
			    }
			  else
			    {
			      varFactor = @"lb*ft/s";
			    }
			}
		      else if ([unit isEqualToString: @"O"])
			{
			  // Temperature
			  varFactor = @"K";
			}
		      else if ([unit isEqualToString: @"1/O"])
			{
			  // Coefficient of thermal expansion
			  varFactor = @"1/K";
			}
		      else if ([unit isEqualToString: @"L2/(T2*O)"])
			{
			  // Specific heat
			  varFactor = @"J/kg*K";
			}
		      else if ([unit isEqualToString: @"(M*L2/T2)/M"])
			{
			  // Latent heat
			  varFactor = @"J/kg";
			}
		      else if ([unit isEqualToString: @"M/(T2*L)"])
			{
			  // Pressure
			  varFactor = @"Pa";
			}
		      else
			{
			  NSLog(@"The dimension %@ is not registered.", unit);
			}
		      
		      data = [data stringByAppendingString: @"@"];
		      data = [data stringByAppendingString: varFactor];
		      data = [data stringByAppendingString: @"@"];
		      data = [data stringByAppendingString: unit];
		      
		      [values addObject: data];
		      if (![unknowns containsObject: data])
			{
			  [unknowns addObject: data];
			}
		    }
		}
	    }
	  // If the field contains a name, just pass it.
	  else
	    {
	      [values addObject: data];
	    }
	}

      // Display the error message, if any.
      if (error)
	{
	  NSString *advice = [NSString stringWithFormat:
			      _(@"\n There isn't a conversion to %s. \n"),
			      [key cString]];
	  [viewer writeMessage: advice];
	  break;
	}
    }

  return error;
}

// Print, at viewer, the unknowns alongside the results.
- (void) printUnknowns: (NSArray *)unknowns withResults: (NSArray *)results
{
  [self printUnknowns: unknowns withResults: results withStatus: nil];
}

/* Print, at viewer, the unknowns alongside the results and the additional info
   (i.e: the state of a beam, tension or compression). */
- (void) printUnknowns: (NSArray *)unknowns withResults: (NSArray *)results
         withStatus: (NSArray *)status
{
  int k;
  double factor, answer;
  NSString *message, *unit, *type, *st;
  NSArray *components;

  for (k = 0; k < [unknowns count]; k++)
    {
      components = [[unknowns objectAtIndex: k] componentsSeparatedByString:
						  @"@"];
      unit = [components objectAtIndex: 1];
      type = [components objectAtIndex: 2];

      if (![unit isEqualToString: @"C"] &&
	  ![unit isEqualToString: @"R"] &&
	  ![unit isEqualToString: @"F"])
	{
	  if ([unit isEqualToString: _(@"degrees")])
	    {
	      factor = [[self conversionFactorFor: @"degrees"
					   ofType: @"degrees"] doubleValue];
	    }
	  else
	    {
	      factor = [[self conversionFactorFor: unit
					   ofType: type] doubleValue];
	    }

	  answer = [[results objectAtIndex: k] doubleValue]/factor;
	}
      else
	{
	  if ([unit isEqualToString: @"C"])
	    {
	      answer = [[results objectAtIndex: k] doubleValue] - 273.15;
	    }
	  else
	    {
	      if ([unit isEqualToString: @"R"])
		{
		  answer = 1.8*([[results objectAtIndex: k] doubleValue]
				- 273.15) + 491.67;
		}
	      else
		{
		  answer = 1.8*([[results objectAtIndex: k] doubleValue]
				- 273.15) + 32;
		}
	    }
	}

      if (status == nil)
	{
	  st = @"";
	}
      else
	{
	  st = [status objectAtIndex: k];
	}

      if ([[components objectAtIndex: 0] hasSuffix: @"#E"])
	{
	  message = [NSString stringWithFormat: @" %@  =  %@  %@ %@;  ",
			      [components objectAtIndex: 0],
			      [self scientificNotationFor: answer],
			      [components objectAtIndex: 1],
			      st];
	}
      else
	{
	  message = [NSString stringWithFormat: @" %@  =  %.3f  %@ %@;  ",
			      [components objectAtIndex: 0],
			      answer,
			      [components objectAtIndex: 1],
			      st];
	}
      
      [viewer writeMessage: message];
    }
}

@end
