/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.exoplatform.services.jcr.impl.core.query.lucene;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * Implements a synonym provider based on a properties file. Each line in the
 * properties file is treated as a synonym definition. Example:
 * <pre>
 * A=B
 * B=C
 * </pre>
 * This synonym provider will return B as a synonym for A and vice versa. The
 * same applies to B and C. However A is not considered a synonym for C, nor
 * C a synonym for A.
 */
public class PropertiesSynonymProvider implements SynonymProvider
{

   /**
    * The logger instance for this class.
    */
   private static final Logger log = LoggerFactory.getLogger("exo.jcr.component.core.PropertiesSynonymProvider");

   /**
    * An empty string array. Returned when no synonym is found.
    */
   private static final String[] EMPTY_ARRAY = new String[0];

   /**
    * Check at most every 10 seconds for configuration updates.
    */
   private static final long CHECK_INTERVAL = 10 * 1000;

   /**
    * The file system resource that contains the configuration.
    */
   private InputStream config;

   /**
    * Timestamp when the configuration was checked last.
    */
   private long lastCheck;

   /**
    * Timestamp when the configuration was last modified.
    */
   private long configLastModified;

   /**
    * Contains the synonym mapping. Map&lt;String, String[]>
    */
   private Map synonyms = new HashMap();

   /**
    * {@inheritDoc}
    */
   public synchronized void initialize(InputStream fsr) throws IOException
   {
      if (fsr == null)
      {
         throw new IOException("PropertiesSynonymProvider requires a path configuration");
      }
      try
      {
         config = fsr;
         synonyms = getSynonyms(config);
         configLastModified = System.currentTimeMillis();
         lastCheck = System.currentTimeMillis();
      }
      catch (IOException e)
      {
         throw e;
      }
   }

   /**
    * {@inheritDoc}
    */
   public String[] getSynonyms(String term)
   {
      checkConfigUpdated();
      term = term.toLowerCase();
      String[] syns;
      synchronized (this)
      {
         syns = (String[])synonyms.get(term);
      }
      if (syns == null)
      {
         syns = EMPTY_ARRAY;
      }
      return syns;
   }

   //---------------------------------< internal >-----------------------------

   /**
    * Checks if the synonym properties file has been updated and this provider
    * should reload the synonyms. This method performs the actual check at most
    * every {@link #CHECK_INTERVAL}. If reloading fails an error is logged and
    * this provider will retry after {@link #CHECK_INTERVAL}.
    */
   private synchronized void checkConfigUpdated()
   {
      if (lastCheck + CHECK_INTERVAL > System.currentTimeMillis())
      {
         return;
      }
      // check last modified
      //try
      //{
         //            if (configLastModified != config.lastModified()) {
         //                synonyms = getSynonyms(config);
         //                configLastModified = config.lastModified();
         //                log.info("Reloaded synonyms from {}", config.getPath());
         //            }
      //}
      //catch (Exception e)
      //{
      //log.error("Exception while reading synonyms", e);
      //}
      // update lastCheck timestamp, even if error occured (retry later)
      lastCheck = System.currentTimeMillis();
   }

   /**
    * Reads the synonym properties file and returns the contents as a synonym
    * Map.
    *
    * @param config the synonym properties file.
    * @return a Map containing the synonyms.
    * @throws IOException if an error occurs while reading from the file system
    *                     resource.
    */
   private static Map getSynonyms(InputStream config) throws IOException
   {
      try
      {
         Map synonyms = new HashMap();
         Properties props = new Properties();
         props.load(config);
         Iterator it = props.entrySet().iterator();
         while (it.hasNext())
         {
            Map.Entry e = (Map.Entry)it.next();
            String key = (String)e.getKey();
            String value = (String)e.getValue();
            addSynonym(key, value, synonyms);
            addSynonym(value, key, synonyms);
         }
         return synonyms;
      }
      catch (Exception e)
      {
         throw Util.createIOException(e);
      }
   }

   /**
    * Adds a synonym definition to the map.
    *
    * @param term     the term
    * @param synonym  synonym for <code>term</code>.
    * @param synonyms the Map containing the synonyms.
    */
   private static void addSynonym(String term, String synonym, Map synonyms)
   {
      term = term.toLowerCase();
      String[] syns = (String[])synonyms.get(term);
      if (syns == null)
      {
         syns = new String[]{synonym};
      }
      else
      {
         String[] tmp = new String[syns.length + 1];
         System.arraycopy(syns, 0, tmp, 0, syns.length);
         tmp[syns.length] = synonym;
         syns = tmp;
      }
      synonyms.put(term, syns);
   }

}
