/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2006 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.fonts.encoding;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.pentaho.reporting.libraries.fonts.LibFontBoot;
import org.pentaho.reporting.libraries.fonts.encoding.manual.Ascii;
import org.pentaho.reporting.libraries.fonts.encoding.manual.BuiltInJavaEncoding;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.resourceloader.factory.property.PropertiesResourceFactory;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A global registry for all supported encodings. This offers the option to fall
 * back to Java - which is enabled by default.
 *
 * @author Thomas Morgner.
 */
public final class EncodingRegistry
{
  private static final Log logger = LogFactory.getLog(EncodingRegistry.class);

  /**
   * Implementation doc: This class uses several sources to load the encodings.
   * The primary source are the known manually written encodings; they provide
   * the base set (namely UFT-16LE and ISO-8859-1).
   *
   * The 'encodingsMapping' hashmap contains the name of the encoding along
   * with the classname. For now, we do not cache these encodings; we should
   * to it, but we dont.
   *
   * The second set are the generated mappings; the hashmap contains the name
   * of the encoding along with the resourcekey pointing to the .ser files.
   * These files must be serialized objects of the type "EncodingData". For
   * now, only External8BitEncodingData objects are accepted.
   *
   * And finally, we use the JDK for all not-known encodings.
   */

  /**
   * the string that should be encoded. This is a simple space, and should be
   * supported by all encodings - but will cause InvalidEncodingExceptions if
   * the encoding does not exist.
   */
  private static final String TEST_STRING = " ";

  private HashMap encodingsMapping;
  private HashMap generatedMapping;
  private HashMap fallbackMapping;
  private HashMap aliases;
  private ResourceManager manager;

  private static EncodingRegistry instance;
  public static final String ENCODING_ALIAS_PREFIX = "org.pentaho.reporting.libraries.fonts.encoding.alias.";

  public static synchronized EncodingRegistry getInstance()
  {
    if (instance == null)
    {
      instance = new EncodingRegistry();
      instance.registerDefaults();
    }
    return instance;
  }

  private EncodingRegistry()
  {
    this.manager = new ResourceManager();
    this.manager.registerFactoryCache();
    this.manager.registerDataCache();
    this.manager.registerDefaultLoaders();
    this.manager.registerFactory(new EncodingFactory());
    this.manager.registerFactory(new PropertiesResourceFactory());

    this.encodingsMapping = new HashMap();
    this.generatedMapping = new HashMap();
    this.fallbackMapping = new HashMap();
    this.aliases = new HashMap();
  }

  private void registerDefaults()
  {
    final Configuration config = LibFontBoot.getInstance().getGlobalConfig();

    final Iterator encodings = config.findPropertyKeys("org.pentaho.reporting.libraries.fonts.encoding.manual.");
    while (encodings.hasNext())
    {
      final String key = (String) encodings.next();
      final String encodingClass = config.getConfigProperty(key);
      final Object maybeEncoding = ObjectUtilities.loadAndInstantiate
              (encodingClass, EncodingRegistry.class, Encoding.class);
      if (maybeEncoding != null)
      {
        // ok, loaded perfectly ..
        final Encoding encoding = (Encoding) maybeEncoding;
        final String name = encoding.getName();
        //Log.debug ("Manual mapping added. " + name);
        encodingsMapping.put(normalizeEncodingName(name), encodingClass);
      }
    }

    final Iterator generateDirs = config.findPropertyKeys("org.pentaho.reporting.libraries.fonts.encoding.generated.");
    while (generateDirs.hasNext())
    {
      final String key = (String) generateDirs.next();
      final String dataLocation = config.getConfigProperty(key);
      try
      {
        final ResourceKey resKey = manager.createKey(dataLocation);
        final Resource res = manager.create(resKey, null, Properties.class);
        final Properties props = (Properties) res.getResource();
        final Iterator vals = props.entrySet().iterator();
        while (vals.hasNext())
        {
          final Map.Entry entry = (Map.Entry) vals.next();
          final String encName = (String) entry.getKey();
          final String encPath = (String) entry.getValue();
          final ResourceKey encKey = manager.deriveKey(resKey, encPath);
          //Log.debug ("Generated mapping added. " +  encName);
          generatedMapping.put(normalizeEncodingName(encName), encKey);
        }
      }
      catch (Exception e)
      {
        // ignore that key, it is behaving badly ..
        e.printStackTrace();
      }
    }

    final Iterator aliasesIt = config.findPropertyKeys
            (ENCODING_ALIAS_PREFIX);
    while (aliasesIt.hasNext())
    {
      final String key = (String) aliasesIt.next();
      final String alias = key.substring(ENCODING_ALIAS_PREFIX.length());
      final String target = config.getConfigProperty(key);
      aliases.put(normalizeEncodingName(alias), normalizeEncodingName(target));
    }
  }

  /**
   * Returns <code>true</code> if the encoding is valid, and <code>false</code>
   * otherwise.
   *
   * @param encoding the encoding (name).
   * @return A boolean.
   */
  public boolean isSupportedEncoding(final String encoding)
  {
    if (encoding == null)
    {
      throw new NullPointerException();
    }

    final String key = normalizeEncodingName(encoding);

    if (encodingsMapping.containsKey(key))
    {
      return true;
    }

    if (generatedMapping.containsKey(key))
    {
      return true;
    }
    if (fallbackMapping.containsKey(key))
    {
      return true;
    }

    return isEncodingSupportedJVM(encoding);
  }

  private String normalizeEncodingName (final String name)
  {
    final String lcName = name.toLowerCase();
    final String retval = lcName.replace('_', '-');

    final String alias = (String) aliases.get(retval);
    if (alias != null)
    {
      return alias;
    }

    return retval;
  }

  private boolean isEncodingSupportedJVM(final String encoding)
  {
    try
    {
      //noinspection ResultOfMethodCallIgnored
      TEST_STRING.getBytes(encoding);
      fallbackMapping.put(encoding, Boolean.TRUE);
      return true;
    }
    catch (Exception ue)
    {
      // There is a known JDK bug in JDK 1.5 with Cp903 encoding
      logger.info("Encoding " + encoding + " is not supported.");
      return false;
    }
  }


  /**
   * Helper method to read the platform default encoding from the VM's system
   * properties.
   *
   * @return the contents of the system property "file.encoding".
   */
  public static String getPlatformDefaultEncoding()
  {
    return LibFontBoot.getInstance().getGlobalConfig().getConfigProperty("file.encoding", "Cp1252");
  }

  public Encoding getEncoding(final String name)
  {
    final String key = normalizeEncodingName(name);
    final String manual = (String) encodingsMapping.get(key);
    if (manual != null)
    {
      // we tested the instantiation during the initialization,
      // so we can be sure that no errors appear here
      return (Encoding) ObjectUtilities.loadAndInstantiate
              (manual, EncodingRegistry.class, Encoding.class);
    }

    final ResourceKey generated = (ResourceKey) generatedMapping.get(key);
    if (generated != null)
    {

      try
      {
        final Resource res = manager.create(generated, null, Encoding.class);
        final Object o = res.getResource();
        if (o instanceof EncodingCore)
        {
          return new ExternalEncoding(name, (EncodingCore) o, res);
        }
      }
      catch (Exception e)
      {
        // fall back ...
        logger.warn("Failed to create external-encoding instance for key " + key, e);
      }
    }
    if (isEncodingSupportedJVM(name))
    {
      return new BuiltInJavaEncoding(name, true);
    }
    else if (isEncodingSupportedJVM(key))
    {
      return new BuiltInJavaEncoding(key, true);
    }
    else
    {
      return new Ascii();
    }
  }

//
//  public static void main(String[] args) throws EncodingException
//  {
//    LibFontBoot.getInstance().start();
//    final EncodingRegistry reg = EncodingRegistry.getInstance();
//    final Encoding encoding = reg.getEncoding("MacRoman");
//    System.out.println ("MacRoman: " + encoding);
//    byte[] text = new byte[] { 'A', 'b', 'c' };
//    final ByteBuffer byteBuffer = new ByteBuffer(text);
//    final CodePointBuffer cp = encoding.decode(byteBuffer, null);
//    System.out.println (Utf16LE.getInstance().encodeString(cp));
//
//  }
}
