/* -*-objc-*-
   GSMarkupCoder.m

   Copyright (C) 2002 Free Software Foundation, Inc.

   Author: Nicola Pero <n.pero@mi.flashnet.it>
   Date: March 2002

   This file is part of GNUstep Renaissance

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.
   
   This library 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 Library General Public
   License along with this library; see the file COPYING.LIB.
   If not, write to the Free Software Foundation,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/ 

#include "GSMarkupCoder.h"
#include "GSMarkupConnector.h"

#ifndef GNUSTEP
# include <Foundation/Foundation.h>
# include "GNUstep.h"
#else
# include <Foundation/NSArray.h>
# include <Foundation/NSData.h>
#endif

/*
 * Return the same string after replacing special chars with their
 * entities - in practice, after replacing:
 * & with &amp;
 * ' with &apos;
 * " with &quot;
 * < with &lt;
 * > with &gt; 
 */
static NSString *_GSMarkupXMLEscapeString (NSString *original)
{
  unichar *string;
  unsigned length = [original length];
  unsigned i, lastSpecialChar;
  NSMutableString *result = [NSMutableString new];

  string = NSZoneMalloc (NSDefaultMallocZone (), sizeof (unichar) * length);
  [original getCharacters: string];

  lastSpecialChar = -1;
  for (i = 0; i < length; i++)
    {
      switch (string[i])
	{
	case '\'':
	case '\"':
	case '<':
	case '>':
	case '&':
	  {
	    if (lastSpecialChar + 1 < i)
	      {
		NSRange r = NSMakeRange (lastSpecialChar + 1, i - (lastSpecialChar + 1));
		[result appendString: [original substringWithRange: r]];
	      }
	    switch (string[i])
	      {
	      case '\'': [result appendString: @"&apos;"]; break;
	      case '\"': [result appendString: @"&quot;"]; break;
	      case '<': [result appendString: @"&lt;"]; break;
	      case '>': [result appendString: @"&gt;"]; break;
	      case '&': [result appendString: @"&amp;"]; break;
	      }
	    lastSpecialChar = i;
	    break;
	  }
	default:
         break;
        }
    }
  if (lastSpecialChar + 1 < length)
    {
      NSRange r = NSMakeRange (lastSpecialChar + 1, length - (lastSpecialChar + 1));
      [result appendString: [original substringWithRange: r]];
    }

  NSZoneFree (NSDefaultMallocZone (), string);
  return result;
}

@implementation GSMarkupCoder

+ (void) encodeObjects: (NSArray *)objects
	    connectors: (NSArray *)connectors
	     nameTable: (NSDictionary *)nameTable
		toFile: (NSString *)file
{
  GSMarkupCoder *coder;
  
  coder = [[self alloc] initWithObjects: objects
			connectors: connectors
			nameTable: nameTable];
  [coder encodeToFile: file];
 
  RELEASE (coder);
  
}

+ (NSData *) encodeObjects: (NSArray *)objects
	        connectors: (NSArray *)connectors
      	         nameTable: (NSDictionary *)nameTable
{
  GSMarkupCoder *coder;
  NSData *result;

  coder = [[self alloc] initWithObjects: objects
			connectors: connectors
			nameTable: nameTable];
  result = [coder encode];
  RETAIN (result);

  RELEASE (coder);
  AUTORELEASE (result);
  return result;
}

- (id) initWithObjects: (NSArray *)objects
	    connectors: (NSArray *)connectors
	     nameTable: (NSDictionary *)nameTable
{
  NSMutableArray *connectorsCopy;
  

  ASSIGN (_objects, objects);

  /* Copy the connectors array since we need to modify it.  */
  connectorsCopy = [connectors mutableCopy];
  ASSIGN (_connectors, connectorsCopy);
  RELEASE (connectorsCopy);

  ASSIGN (_nameTable, nameTable);

  ASSIGN (_objectClassToTagName, [NSMutableDictionary dictionary]);
  ASSIGN (_connectorClassToTagName, [NSMutableDictionary dictionary]);

  return self;
}

- (void) dealloc
{
  RELEASE (_objects);
  RELEASE (_connectors);
  RELEASE (_nameTable);
  RELEASE (_objectClassToTagName);
  RELEASE (_connectorClassToTagName);
  [super dealloc];
}

- (NSData*) encode
{
  int		i;
  int		count;
  NSData	*data;

  ASSIGN (_output, [NSMutableString new]);

  /* XML preamble.  */
  [_output appendString:
    @"<?xml version=\"1.0\"?>\n<!DOCTYPE gsmarkup>\n<gsmarkup>\n\n"];

  /* <objects> section.  */
  [_output appendString: @"<objects>\n"];

  count = [_objects count];

  for (i = 0; i < count; i++)
    {
      [self encodeObject: [_objects objectAtIndex: i]];
    }
  
  [_output appendString: @"</objects>\n\n"];
  
  /* <connectors> section.  */
  [_output appendString: @"<connectors>\n"];

  count = [_connectors count];

  for (i = 0; i < count; i++)
    {
      [self encodeConnector: [_connectors objectAtIndex: i]];
    }

  [_output appendString: @"</connectors>\n\n"];
  
  /* XML postamble.  */
  [_output appendString: @"</gsmarkup>\n"];
 
  data = [_output dataUsingEncoding: NSUTF8StringEncoding];
  DESTROY (_output);
  return data;
}

- (BOOL) encodeToFile: (NSString *)file
{
  NSData	*d = [self encode];

  return [d writeToFile: file atomically: YES];
}

- (void) indent
{
  int i;
  
  for (i = 0; i < _indentation; i++)
    {
      [_output appendString: @" "];
    }  
}

- (void) encodeObject: (id <GSMarkupCoding>)object
{
  NSMutableDictionary *attributes;
  NSString *tagName;
  NSArray *content;
  
  tagName = [self tagNameForObjectClass: [object class]];

  if ([object attributes] != nil)
    {
      attributes = [[object attributes] mutableCopy];
    }
  else
    {
      attributes = [NSMutableDictionary new];
    }
  
  {
    NSArray *keys;

    keys = [_nameTable allKeysForObject: object];
    if (keys != nil  &&  [keys count] > 0)
      {
	int i;
	NSString *idName = [keys objectAtIndex: 0];
	[attributes setObject: idName  forKey: @"id"];
    
	/* Now search the connectors for outlet connectors having this
	 * object as the source.  */
	for (i = [_connectors count] - 1; i > -1; i--)
	  {
	    GSMarkupConnector *connector = [_connectors objectAtIndex: i];
	    
	    if ([connector isKindOfClass: [GSMarkupOutletConnector class]])
	      {
		NSString *source = [connector source];
		
		if ([source isEqualToString: idName])
		  {
		    /* Found!  Encode this into the object itself!  */
		    [attributes setObject: 
				  [NSString stringWithFormat: @"#%@", 
					    [connector target]]
				forKey: [connector label]];
		    /* No longer need to encode this separately.  */
		    [_connectors removeObjectAtIndex: i];
		  }
	      }
	    else if ([connector isKindOfClass:
	      [GSMarkupControlConnector class]])
	      {
		NSString *source = [connector source];
		
		if ([source isEqualToString: idName])
		  {
		    /* Found!  Encode this into the object itself!  */
		    [attributes setObject: 
				  [NSString stringWithFormat: @"#%@", 
					    [connector target]]
				forKey: @"target"];
		    [attributes setObject: [connector label]
				forKey: @"action"];

		    /* No longer need to encode this separately.  */
		    [_connectors removeObjectAtIndex: i];
		  }
	      }
	  }
      }
  }
  
  /* Now build the start tag.  */
  _indentation += 2;
  [self indent];
  [_output appendString: @"<"];
  [_output appendString: tagName];
  
  {
    NSEnumerator *e = [attributes keyEnumerator];
    NSString *key;
    
    while ((key = [e nextObject]) != nil)
      {
	NSString *value = [attributes objectForKey: key];
	
	[_output appendString: @" "];
	[_output appendString: _GSMarkupXMLEscapeString (key)];
	[_output appendString: @"=\""];
	[_output appendString: _GSMarkupXMLEscapeString (value)];
	[_output appendString: @"\""];
      }
    /* Do not close the start tag yet ... if there is no content, we
     * want to produce a start/close tag, such as <button />  */
  }

  RELEASE (attributes);

  /* Now encode the content.  */
  content = [object content];

  if (content != nil  &&  [content count] > 0)
    {
      int i, count = [content count];
      
      [_output appendString: @">\n"];

      for (i = 0; i < count; i++)
	{
	  [self encodeObject: [content objectAtIndex: i]];
	}
      
      /* Now build the end tag.  */
      [self indent];
      [_output appendString: @"</"];
      [_output appendString: tagName];
      [_output appendString: @">\n"];
    }
  else
    {
      [_output appendString: @" />\n"];
    }
  _indentation -= 2;
}

- (void) encodeConnector: (GSMarkupConnector *)connector
{
  NSDictionary *attributes;
  NSString *tagName;
  
  tagName = [self tagNameForConnectorClass: [connector class]];

  attributes = [connector attributes];
  
  /* Now build the tag head.  */
  _indentation += 2;
  [self indent];
  [_output appendString: @"<"];
  [_output appendString: tagName];
  
  /* Now output the attributes.  */
  {
    NSEnumerator *e = [attributes keyEnumerator];
    NSString *key;
    
    while ((key = [e nextObject]) != nil)
      {
	NSString *value = [attributes objectForKey: key];
	
	/* FIXME - escape the output strings! */
	[_output appendString: @" "];
	[_output appendString: key];
	[_output appendString: @"=\""];
	[_output appendString: value];
	[_output appendString: @"\""];
      }
  }

  /* Now close the tag.  */
  [_output appendString: @" />\n"];
  _indentation -= 2;
}

- (NSString *) tagNameForObjectClass: (Class)c
{
  NSString *className = NSStringFromClass (c);
  NSString *tagName;
  
  tagName = [_objectClassToTagName objectForKey: className];
  if (tagName != nil)
    {
      return tagName;
    }

  if ([c respondsToSelector: @selector(tagName)])
    {
      return [c tagName];
    }

  return @"tag";
}

- (NSString *) tagNameForConnectorClass: (Class)c
{
  NSString *className = NSStringFromClass (c);
  NSString *tagName;
  
  tagName = [_connectorClassToTagName objectForKey: className];
  if (tagName != nil)
    {
      return tagName;
    }

  if ([c respondsToSelector: @selector(tagName)])
    {
      return [c tagName];
    }

  /* Else, encode it as an outlet.  */
  return @"outlet";
}

- (void) setTagName: (NSString *)tagName
     forObjectClass: (NSString *)className
{
  [_objectClassToTagName setObject: tagName  forKey: className];
}


- (void) setTagName: (NSString *)tagName
  forConnectorClass: (NSString *)className
{
  [_connectorClassToTagName setObject: tagName  forKey: className];
}

@end


