content: package rabbitescape.engine.config;
  
  import static rabbitescape.engine.util.Util.*;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.SortedSet;
  import java.util.TreeMap;
  import java.util.TreeSet;
  
  import rabbitescape.engine.err.RabbitEscapeException;
  import rabbitescape.engine.util.Util.Function;
  
  public class ConfigTools
  {
      public static class ConfigParsingError extends RabbitEscapeException
      {
          private static final long serialVersionUID = 1L;
          public String configValue;
          public int charNumber;
      }
  
      public static class InvalidValueType extends ConfigParsingError
      {
          private static final long serialVersionUID = 1L;
  
          public final Class<?> expectedClass;
          public final Class<?> actualClass;
  
          public InvalidValueType(
              Class<?> expectedClass,
              Class<?> actualClass
          )
          {
              this.expectedClass = expectedClass;
              this.actualClass = actualClass;
          }
      }
  
      public static class InvalidValue extends ConfigParsingError
      {
          private static final long serialVersionUID = 1L;
  
          public final String value;
          public final Class<?> clazz;
  
          public InvalidValue( String value, Class<?> clazz )
          {
              this.value = value;
              this.clazz = clazz;
          }
      }
  
      public static class UnexpectedCharacter extends ConfigParsingError
      {
          private static final long serialVersionUID = 1L;
  
          public final String expectedChars;
          public final char actualChar;
  
          public UnexpectedCharacter( String expectedChars, char actualChar )
          {
              this.expectedChars = expectedChars;
              this.actualChar = actualChar;
          }
      }
  
      public static class UnknownValueType extends RabbitEscapeException
      {
          private static final long serialVersionUID = 1L;
  
          public final Class<?> clazz;
  
          public UnknownValueType( Class<?> clazz )
          {
              this.clazz = clazz;
          }
      }
  
      public static void setInt( Config config, String key, int value )
      {
          config.set( key, String.valueOf( value ) );
      }
  
      public static int getInt( Config config, String key )
      {
          return Integer.parseInt( config.get( key ) );
      }
  
      public static void setBool( Config config, String key, boolean value )
      {
          config.set( key, String.valueOf( value ) );
      }
  
      public static boolean getBool( Config config, String key )
      {
          return Boolean.parseBoolean( config.get( key ) );
      }
  
      public static void setString( Config config, String key, String value )
      {
          config.set( key, value );
      }
  
      public static String getString( Config config, String key )
      {
          return config.get(  key );
      }
  
      // TODO: break into separate class file
      public static <T> Map<String, T> getMap(
          Config cfg, String configKey, Class<T> clazz )
      {
          return stringToMap( cfg.get( configKey ), clazz );
      }
  
      public static <T> Map<String, T> stringToMap(
          String jsonish, Class<T> clazz )
      {
          final int open_bracket = 0;
          final int comma_or_close_bracket = 1;
          final int end = 2;
          final int key_open_quote_or_close_bracket = 3;
          final int key_char_or_close_quote = 4;
          final int colon = 5;
          final int value_open_quote_or_digit = 6;
          final int value_char_or_close_quote = 7;
          final int value_digit_or_comma_or_close_bracket = 8;
          final int key_open_quote = 9;
  
          int mode = open_bracket;
  
          StringBuilder key = new StringBuilder();
          StringBuilder value = new StringBuilder();
          Map<String, T> ret = new TreeMap<String, T>();
  
          int i = 0;
          for ( char ch : asChars( jsonish ) )
          {
              try
              {
                  switch ( mode )
                  {
                      case open_bracket:
                          expect( "{", ch );
                          mode = key_open_quote_or_close_bracket;
                          break;
                      case key_open_quote:
                          expect( "\"", ch );
                          mode = key_char_or_close_quote;
                          key.setLength( 0 );
                          value.setLength( 0 );
                          break;
                      case key_open_quote_or_close_bracket:
                          expect( "\"}", ch );
                          if ( ch == '"' )
                          {
                              mode = key_char_or_close_quote;
                              key.setLength( 0 );
                              value.setLength( 0 );
                          }
                          else
                          {
                              mode = end;
                          }
                          break;
                      case key_char_or_close_quote:
                          if ( ch == '"' )
                          {
                              mode = colon;
                          }
                          else
                          {
                              key.append( ch );
                          }
                          break;
                      case colon:
                          expect( ":", ch );
                          mode = value_open_quote_or_digit;
                          break;
                      case value_open_quote_or_digit:
                          expect( "\"0123456789", ch );
                          if ( ch == '"' )
                          {
                              expectValueTypeIs( clazz, String.class );
                              mode = value_char_or_close_quote;
                          }
                          else
                          {
                              expectValueTypeIs( clazz, Integer.class );
                              mode = value_digit_or_comma_or_close_bracket;
                              value.append( ch );
                          }
                          break;
                      case value_char_or_close_quote:
                          if ( ch == '"' )
                          {
                              mode = comma_or_close_bracket;
                          }
                          else
                          {
                              value.append( ch );
                          }
                          break;
                      case comma_or_close_bracket:
                          expect( ",}", ch );
                          foundValue( clazz, key, value, ret );
                          if ( ch == ',' )
                          {
                              mode = key_open_quote;
                          }
                          else // '}'
                          {
                              mode = end;
                          }
                          break;
                      case value_digit_or_comma_or_close_bracket:
                          if ( ch == ',' )
                          {
                              foundValue( clazz, key, value, ret );
                              mode = key_open_quote;
                          }
                          else if ( ch == '}' )
                          {
                              foundValue( clazz, key, value, ret );
                              mode = end;
                          }
                          else
                          {
                              expect( "0123456789", ch );
                              value.append( ch );
                          }
                          break;
                      case end:
                          expect( "", ch ); // Throw - should have ended!
                          break;
                  }
  
                  ++i;
              }
              catch( ConfigParsingError e )
              {
                  e.charNumber = i;
                  e.configValue = jsonish;
                  throw e;
              }
          }
          return ret;
      }
  
      private static <T> void foundValue(
          Class<T> clazz,
          StringBuilder key,
          StringBuilder value,
          Map<String, T> ret )
      {
          ret.put( key.toString(), makeValue( value, clazz ) );
      }
  
      private static void expectValueTypeIs(
          Class<?> expectedClass, Class<?> actualClass )
      {
          if ( expectedClass != actualClass )
          {
              throw new InvalidValueType( expectedClass, actualClass );
          }
      }
  
      private static void expect( String exp, char act )
      {
          if ( exp.indexOf( act ) == -1 )
          {
              throw new UnexpectedCharacter( exp, act );
          }
      }
  
      @SuppressWarnings( "unchecked" )
      private static <T> T makeValue( StringBuilder value, Class<T> clazz )
      {
          String v = value.toString();
  
          if ( clazz == String.class )
          {
              return (T)v;
          }
          else if ( clazz == Integer.class )
          {
              try
              {
                  return (T)new Integer( v );
              }
              catch ( NumberFormatException e )
              {
                  throw new InvalidValue( v, clazz );
              }
          }
          else
          {
              throw new UnknownValueType( clazz );
          }
      }
  
      public static <T> void setMap(
          Config cfg, String key, Map<String, T> value )
      {
          cfg.set( key, mapToString( value ) );
      }
  
      public static <T> String mapToString( Map<String, T> value )
      {
          StringBuilder val = new StringBuilder();
          val.append( '{' );
  
          boolean stringVals = true;
          boolean first = true;
          List<String> ks = new ArrayList<String>( value.keySet() );
          Collections.sort( ks );
          for ( String k : ks )
          {
              T rhs = value.get( k );
  
              if ( first )
              {
                  first = false;
                  stringVals = rhs instanceof String;
              }
              else
              {
                  val.append( ',' );
              }
  
              val.append( '"' );
              val.append( k );
              val.append( "\":" );
              if ( stringVals ) val.append( '"' );
              val.append( rhs );
              if ( stringVals ) val.append( '"' );
          }
  
          val.append( '}' );
  
          return val.toString();
      }
  
      /**
       * Note: does not support negative numbers.
       */
      public static <T> Set<T> getSet(
          Config cfg, String configKey, Class<T> clazz )
      {
          return stringToSet( cfg.get( configKey ), clazz );
      }
  
      /**
       * Note: does not support negative numbers.
       */
      public static <T> void setSet( Config cfg, String configKey, Set<T> value )
      {
          cfg.set( configKey, setToString( new TreeSet<T>( value ) ) );
      }
  
      public static <T> String setToString( SortedSet<T> ret )
      {
          return "[" + join( ",", map( quoteString( ret ), ret ) ) + "]";
      }
  
      private static <T> Function<T, String> quoteString( SortedSet<T> set )
      {
          return new Function<T, String>()
          {
              @Override
              public String apply( T t )
              {
                  if ( t instanceof String )
                  {
                      return "\"" + t.toString() + "\"";
                  }
                  else
                  {
                      return t.toString();
                  }
              }
          };
      }
  
      private static <T> Set<T> stringToSet( String jsonish, Class<T> clazz )
      {
          final int open_square_bracket = 0;
          final int open_quote_or_digit_or_close_square_bracket = 1;
          final int char_or_close_quote = 2;
          final int end = 3;
          final int digit_or_comma_or_close_square_bracket = 4;
          final int comma_or_close_square_bracket = 5;
          final int digit_or_open_quote = 6;
  
          int mode = open_square_bracket;
  
          boolean foundValue = false;
          StringBuilder value = new StringBuilder();
          Set<T> ret = new TreeSet<T>();
  
          int i = 0;
          for ( char ch : asChars( jsonish ) )
          {
              try
              {
                  switch ( mode )
                  {
                      case open_square_bracket:
                          expect( "[", ch );
                          mode = open_quote_or_digit_or_close_square_bracket;
                          break;
                      case open_quote_or_digit_or_close_square_bracket:
                          expect( "\"0123456789]", ch );
                          if ( ch == '"' )
                          {
                              expectValueTypeIs( clazz, String.class );
                              foundValue = true;
                              value.setLength( 0 );
                              mode = char_or_close_quote;
                          }
                          else if ( ch == ']' )
                          {
                              maybeFoundValue( clazz, value, foundValue, ret );
                              mode = end;
                          }
                          else
                          {
                              expectValueTypeIs( clazz, Integer.class );
                              foundValue = true;
                              value.setLength( 0 );
                              value.append( ch );
                              mode = digit_or_comma_or_close_square_bracket;
                          }
                          break;
                      case char_or_close_quote:
                          if ( ch == '"' )
                          {
                              maybeFoundValue( clazz, value, foundValue, ret );
                              mode = comma_or_close_square_bracket;
                          }
                          else
                          {
                              value.append( ch );
                          }
                          break;
                      case comma_or_close_square_bracket:
                          expect( ",]", ch );
                          if ( ch == ',' )
                          {
                              maybeFoundValue( clazz, value, foundValue, ret );
                              mode = digit_or_open_quote;
                          }
                          else
                          {
                              mode = end;
                          }
                          break;
                      case digit_or_comma_or_close_square_bracket:
                          expect( "0123456789,]", ch );
                          if ( ch == ',' )
                          {
                              maybeFoundValue( clazz, value, foundValue, ret );
                              mode = digit_or_open_quote;
                          }
                          else if ( ch == ']' )
                          {
                              maybeFoundValue( clazz, value, foundValue, ret );
                              mode = end;
                          }
                          else
                          {
                              value.append( ch );
                          }
                          break;
                      case digit_or_open_quote:
                          expect( "0123456789\"", ch );
                          if ( ch == '"' )
                          {
                              expectValueTypeIs( clazz, String.class );
                              foundValue = true;
                              value.setLength( 0 );
                              mode = char_or_close_quote;
                          }
                          else
                          {
                              expectValueTypeIs( clazz, Integer.class );
                              foundValue = true;
                              value.setLength( 0 );
                              value.append( ch );
                              mode = digit_or_comma_or_close_square_bracket;
                          }
                          break;
                      case end:
                          expect( "", ch ); // Throw - should have ended!
                          break;
                  }
  
                  ++i;
              }
              catch( ConfigParsingError e )
              {
                  e.charNumber = i;
                  e.configValue = jsonish;
                  throw e;
              }
          }
          return ret;
      }
  
      @SuppressWarnings( "unchecked" )
      private static <T> void maybeFoundValue(
          Class<T> clazz, StringBuilder value, boolean foundValue, Set<T> ret )
      {
          if ( foundValue )
          {
              ret.add( makeValue( value, clazz ) );
          }
      }
  }
  