/*
 * 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.resourceloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.base.util.CSVQuoter;
import org.pentaho.reporting.libraries.base.util.CSVTokenizer;
import org.pentaho.reporting.libraries.base.util.IOUtils;

/**
 * Provides a setup of utility methods for operating on a ResourceKey class.
 *
 * @author David Kincade
 */
public class ResourceKeyUtils
{
  private static final String DELIMITER = ";";
  private static final String SERIALIZATION_PREFIX = "resourcekey:";
  private static final Log logger = LogFactory.getLog(ResourceManager.class);

  /**
   * Returns a string representation of the ResourceKey based on the pieces that are passed as parameters
   *
   * @param schema            the string representation of the schema
   * @param identifier        the string representation of the identifier
   * @param factoryParameters the set of factory parameters (<code>null</code> allowed)
   * @return the string version with the pieces delimited and concatenated
   */
  public static String createStringResourceKey(final String schema,
                                               final String identifier,
                                               final Map factoryParameters)
  {
    final String factoryParamString = convertFactoryParametersToString(factoryParameters);
    final CSVQuoter quoter = new CSVQuoter(';');
    return quoter.doQuoting(SERIALIZATION_PREFIX + schema) + DELIMITER +
        quoter.doQuoting(identifier) +
        (factoryParamString == null ? "" : DELIMITER + quoter.doQuoting(factoryParamString));
  }

  /**
   * Parses the string version of the Resource Key into the components
   *
   * @return
   */
  public static ResourceKeyData parse(final String resourceKeyString) throws ResourceKeyCreationException
  {
    if (resourceKeyString == null)
    {
      throw new IllegalArgumentException("Source string can not be null");
    }
    if (!resourceKeyString.startsWith(SERIALIZATION_PREFIX))
    {
      throw new ResourceKeyCreationException("The source string does not start with the string ["
          + SERIALIZATION_PREFIX + "]");
    }

    final CSVTokenizer tokenizer = new CSVTokenizer(resourceKeyString, DELIMITER);
    if (tokenizer.hasMoreElements() == false)
    {
      throw new ResourceKeyCreationException("Schema is missing");
    }
    final String rawSchema = tokenizer.nextToken();
    if (rawSchema.startsWith(SERIALIZATION_PREFIX) == false)
    {
      throw new ResourceKeyCreationException("Prefix is wrong");
    }
    final String schema = rawSchema.substring(SERIALIZATION_PREFIX.length());

    if (tokenizer.hasMoreElements() == false)
    {
      throw new ResourceKeyCreationException("Identifier is missing");
    }
    final String id = tokenizer.nextToken();
    final Map parameters;
    if (tokenizer.hasMoreElements())
    {
      parameters = parseFactoryParametersFromString(tokenizer.nextToken());
    }
    else
    {
      parameters = null;
    }

    // The 1st component is the schema... the 2nd is the identifier... the 3rd is the factory parameters (optional)
    return new ResourceKeyData(schema, id, parameters);
  }

  /**
   * Returns the list of factory parameters for the specified ResourceKey as a String
   * representation in the format:
   * <pre>
   *   key=value:key=value:...:key=value
   * </pre>
   * The colon (:) is the separator between parameters and the equal sign (=) is the separator
   * between the key and the value.
   * <p/>
   * If the factory parameters is empty, <code>null</code> will be returned
   *
   * @param factoryParameters the parameter map.
   * @return a String representation of the factory parameters for the ResourceKey
   */
  public static String convertFactoryParametersToString(final Map factoryParameters)
  {
    if (factoryParameters == null || factoryParameters.size() <= 0)
    {
      return null;
    }

    final CSVQuoter innerQuoter = new CSVQuoter('=');
    final CSVQuoter quoter = new CSVQuoter(':');
    final StringBuffer sb = new StringBuffer();
    for (Iterator iterator = factoryParameters.keySet().iterator(); iterator.hasNext();)
    {
      if (sb.length() > 0)
      {
        sb.append(':');
      }
      
      final StringBuffer entrySb = new StringBuffer();
      Object key = iterator.next();
      if (key instanceof FactoryParameterKey)
      {
        FactoryParameterKey fkey = (FactoryParameterKey) key;
        entrySb.append(innerQuoter.doQuoting("f:" + fkey.getName()));
      }
      else if (key instanceof LoaderParameterKey)
      {
        LoaderParameterKey fkey = (LoaderParameterKey) key;
        entrySb.append(innerQuoter.doQuoting("l:" + fkey.getName()));
      }
      else
      {
        throw new IllegalArgumentException();
      }

      Object value = factoryParameters.get(key);
      entrySb.append('=');
      if (value != null)
      {
        // todo: This String.valueOf is probably and very likely wrong
        entrySb.append(innerQuoter.doQuoting(String.valueOf(value)));
      }

      sb.append(quoter.doQuoting(entrySb.toString()));
    }
    logger.debug("Converted ResourceKey's Factory Parameters to String: [" + sb.toString() + "]");
    return sb.toString();
  }

  /**
   * Returns a Map of parameters based on the input string. The string will be parsed using the
   * same format as defined in the <code>getFactoryParametersAsString()</code> method.
   *
   * @param factoryParameters the String representation of factory parameters
   * @return a Map of factory parameters parsed from the string, or <code>null</code>
   *         if the source string was null or contained no data
   */
  public static Map parseFactoryParametersFromString(final String factoryParameters)
  {
    if (factoryParameters == null)
    {
      return null;
    }
    final Map params = new HashMap();
    final CSVTokenizer tokenizer = new CSVTokenizer(factoryParameters, ":");
    while (tokenizer.hasMoreTokens())
    {
      final String entry = tokenizer.nextToken();
      final CSVTokenizer innerTokenizer = new CSVTokenizer(entry, "=");
      final Object key;
      if (innerTokenizer.hasMoreElements())
      {
        final String keyString = innerTokenizer.nextToken();
        if (keyString.startsWith("f:"))
        {
          key = new FactoryParameterKey(keyString.substring(2));
        }
        else if (keyString.startsWith("l:"))
        {
          key = new LoaderParameterKey(keyString.substring(2));
        }
        else
        {
          throw new IllegalStateException();
        }
      }
      else
      {
        throw new IllegalStateException();
      }
      final Object value;
      if (innerTokenizer.hasMoreElements())
      {
        value = innerTokenizer.nextToken();
      }
      else
      {
        value = null;
      }
      params.put(key, value);
    }

    if (logger.isDebugEnabled())
    {
      logger.debug("Converted ResourceKey's Factory Parameter String to a Map: [" + factoryParameters
          + "] -> map of size " + params.size());
    }
    return params;
  }

  /**
   * Returns the schema portion of the serialized ResourceKey string. If the string is invalid,
   * <code>null</code> will be returned.
   *
   * @param data the String serialized version of a <code>ResourceKey</code>
   * @return the schema object.
   */
  public static Object readSchemaFromString(final String data)
  {
    if (data == null)
    {
      return null;
    }
    final CSVTokenizer tokenizer = new CSVTokenizer(data, DELIMITER);
    if (tokenizer.hasMoreElements() == false)
    {
      return null;
    }

    final String tempData = tokenizer.nextToken();
    if (tempData.startsWith(SERIALIZATION_PREFIX))
    {
      return tempData.substring(SERIALIZATION_PREFIX.length());
    }
    return null;
  }

  /**
   * Performs a simple attempt at a "smart" conversion to a ResourceKey.
   * <ol>
   * <li>If the <code>value</code> is <code>null</code>, this method will return <code>null</code></li>
   * <li>If the <code>value</code> is a <code>ResourceKey</code>, the <code>value</code> will be returned</li>
   * <li>If the <code>value</code> is a <code>String</code> and is syntactically valid as a <code>URL</code>,
   * it will be converted to a <code>URL</code> and then used to create a <code>ResourceKey</code></li>
   * <li>If the <code>value</code> is a <code>String</code> and is NOT syntactically valid as a <code>URL</code>,
   * it will be converted ot a <code>File</code> and then used to create a <code>ResourceKey</code></li>
   * <li>All other types will be passed along to attempt a key creation as is
   * </ol>
   *
   * @param value           the object to convert to a <code>ResourceKey</code>
   * @param resourceManager the resource manager used in key creation
   * @param parameters      the parameters that should be passed to generate a resource key
   * @return the resource key created
   * @throws ResourceKeyCreationException indicates the value can not be used to create a Resource Key
   */
  public static ResourceKey toResourceKey(final Object value,
                                          final ResourceManager resourceManager,
                                          final ResourceKey contextKey,
                                          final Map parameters)
      throws ResourceKeyCreationException
  {
    if (value == null)
    {
      return null;
    }

    if (value instanceof ResourceKey)
    {
      return (ResourceKey) value;
    }

    if (resourceManager == null)
    {
      throw new NullPointerException("ResourceManager is null");
    }

    // If the value is a String, try a URL or a File
    Object tempObject = value;
    if (tempObject instanceof String)
    {
      final String spec = (String) value;
      if (contextKey != null)
      {
        try
        {
          return resourceManager.deriveKey(contextKey, spec, parameters);
        }
        catch (ResourceKeyCreationException e)
        {
          // ignored ..
        }
      }
      try
      {
        tempObject = new URL(spec);
      }
      catch (MalformedURLException e)
      {
        tempObject = new File(spec);
      }
    }

    return resourceManager.createKey(tempObject, parameters);
  }

  /**
   * Returns a new ResourceKey with the specified source resource embedded inside as a byte []
   *
   * @param source            the ResourceKey to the source which will be embedded - NOTE: the pattern can specify an exact name
   *                          or a pattern for creating a temporary name. If the name exists, it will be replaced.
   * @param factoryParameters any factory parameters which should be added to the ResourceKey being created
   * @return the ResourceKey for the newly created embedded entry
   */
  public static ResourceKey embedResourceInKey(final ResourceManager manager,
                                               final ResourceKey source,
                                               final Map factoryParameters)
      throws IOException, ResourceKeyCreationException, ResourceLoadingException
  {
    if (manager == null)
    {
      throw new IllegalArgumentException();
    }
    if (source == null)
    {
      throw new IllegalArgumentException();
    }
    final ResourceData resourceData = manager.load(source);

    // Load the resource into a byte array
    final InputStream in = resourceData.getResourceAsStream(manager);
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    try
    {
      IOUtils.getInstance().copyStreams(in, out);
      // Create a resource key with the byte array
      return manager.createKey(out.toByteArray(), factoryParameters);
    }
    finally
    {
      try
      {
        in.close();
      }
      catch (IOException e)
      {
        logger.error("Error closing input stream", e);
      }
    }
  }
}
