/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Nam Nguyen
 */

package com.caucho.quercus.lib.gettext;

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.env.*;
import com.caucho.quercus.lib.string.StringModule;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.util.L10N;
import com.caucho.util.LruCache;
import com.caucho.vfs.Path;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.logging.Logger;


/**
 * Module to find translated strings and return them in desired charset.
 * Translations are LRU cached.
 */
public class GettextModule
  extends AbstractQuercusModule
{
  private LruCache<Object,GettextResource> _cache
    = new LruCache<Object,GettextResource>(16);

  private final Logger log
    = Logger.getLogger(GettextModule.class.getName());
  private final L10N L = new L10N(GettextModule.class);

  public String []getLoadedExtensions()
  {
    return new String[] { "gettext" };
  }

  /**
   * Sets charset of translated strings that are returned from this domain.
   *
   * @param env
   * @param domain
   * @param codeset
   * @return codeset
   */
  public String bind_textdomain_codeset(Env env,
                                             String domainName,
                                             String charset)
  {
    getDomain(env, domainName).setCharset(charset);
    
    return charset;
  }

  /**
   * Changes root directory of domain.
   *
   * @param env
   * @param domain
   * @param directory
   * @return directory
   */
  public Value bindtextdomain(Env env,
                              String domainName,
                              StringValue directory)
  {
    GettextDomain domain = getDomain(env, domainName);
    
    domain.setPath(env, directory);
    
    if (domain.getPath() == null)
      return BooleanValue.FALSE;
    else
      return directory;
  }

  /**
   * Same as gettext, but allows overriding of domain and category.
   *
   * @param env
   * @param domain
   * @param message
   * @param category
   */
  public StringValue dcgettext(Env env,
                              String domainName,
                              StringValue message,
                              int category,
                              Value []args)
  {
    return translate(env,
                     getDomain(env, domainName),
                     getCategory(env, category),
                     message,
                     args);
  }

  /**
   * Same as ngettext, but allows overriding of domain and category.
   *
   * @param env
   * @param domain
   * @param msgid1
   * @param msgid2
   * @param n
   * @param category
   */
  public StringValue dcngettext(Env env,
                                String domainName,
                                StringValue msgid1,
                                StringValue msgid2,
                                int n,
                                int category,
                                Value []args)
  {
    return translate(env,
                     getDomain(env, domainName),
                     getCategory(env, category),
                     msgid1,
                     msgid2,
                     n,
                     args);
  }

  /**
   * Same as gettext, but allows overriding of current domain.
   *
   * @param env
   * @param domain
   * @param message
   */
  public StringValue dgettext(Env env,
                              String domainName,
                              StringValue message,
                              Value []args)
  {
    return translate(env,
                     getDomain(env, domainName),
                     "LC_MESSAGES",
                     message,
                     args);
  }

  /**
   * Same as ngettext, but allows overriding of current domain.
   *
   * @param env
   * @param domain
   * @param msgid1
   * @param msgid2
   * @param n
   */
  public StringValue dngettext(Env env,
                               String domainName,
                               StringValue msgid1,
                               StringValue msgid2,
                               int n,
                               Value []args)
  {
    return translate(env,
                     getDomain(env, domainName),
                     "LC_MESSAGES",
                     msgid1,
                     msgid2,
                     n,
                     args);
  }

  /**
   * Alias of gettext().
   *
   * @param env
   * @param message
   */
  public StringValue _(Env env, StringValue message, Value []args)
  {
    return gettext(env, message, args);
  }

  /**
   * Returns translated string from current domain and default category.
   *
   * @param env
   * @param message
   */
  public StringValue gettext(Env env, StringValue message, Value []args)
  {
    return translate(env,
                     getCurrentDomain(env),
                     "LC_MESSAGES",
                     message,
                     args);
  }

  /**
   * Returns translated plural string form from current domain and default
   * category.
   *
   * @param env
   * @param msgid1
   * @param msgid2
   * @param n
   * @return translated string, or original plural string if n == 1,
   *     else return original singular string
   */
  public StringValue ngettext(Env env,
                              StringValue msgid1,
                              StringValue msgid2,
                              int n,
                              Value []args)
  {
    return translate(env,
                     getCurrentDomain(env),
                     "LC_MESSAGES",
                     msgid1,
                     msgid2,
                     n,
                     args);
  }

  /**
   * Changes the current domain.
   *
   * @param env
   * @param domain
   * @return name of current domain after change.
   */
  public String textdomain(Env env,
                                @Optional Value domain)
  {
    if (! domain.isNull()) {
      String name = domain.toString();
      
      setCurrentDomain(env, name);
      
      return name;
    }
    
    return getCurrentDomain(env).getName();
  }

  /**
   * Retrieves the translation for message.
   * 
   * @param env
   * @param domain
   * @param category
   * @param message
   *
   * @return translation found, else message
   */
  private StringValue translate(Env env,
                                GettextDomain domain,
                                CharSequence category,
                                StringValue message,
                                Value []args)
  {
    Locale locale = env.getLocaleInfo().getMessages().getLocale();

    GettextResource resource = getResource(env,
                                           domain.getPath(),
                                           locale,
                                           category,
                                           domain.getName());

    StringValue unicodeTranslation = resource.getTranslation(message);

    if (unicodeTranslation == null)
      unicodeTranslation = message;

    StringValue translation
      = message.create(env, unicodeTranslation, domain.getCharset());
    
    return format(env, translation, args);
  }

  /**
   * Retrieves the plural translation for msgid1.
   * 
   * @param env
   * @param domain
   * @param category
   * @param msgid1
   * @param msgid2
   *
   * @return translation found, else msgid1 if n == 1, else msgid2
   */
  private StringValue translate(Env env,
                                GettextDomain domain,
                                CharSequence category,
                                StringValue msgid1,
                                StringValue msgid2,
                                int quantity,
                                Value []args)
  {
    Locale locale = env.getLocaleInfo().getMessages().getLocale();

    GettextResource resource = getResource(env,
                                           domain.getPath(),
                                           locale,
                                           category,
                                           domain.getName());

    StringValue unicodeTranslation
      = resource.getTranslation(msgid1, quantity);

    if (unicodeTranslation == null)
      unicodeTranslation = errorReturn(msgid1, msgid2, quantity);

    StringValue translation = msgid1.create(
        env, unicodeTranslation, domain.getCharset());
    
    return format(env, translation, args);
  }

  private GettextResource getResource(Env env,
                              Path path,
                              Locale locale,
                              CharSequence category,
                              String domain)
  {
    ArrayList<Object> key = new ArrayList<Object>();

    key.add(path.getFullPath());
    key.add(locale);
    key.add(category);
    key.add(domain);

    GettextResource resource = _cache.get(key);

    if (resource == null) {
      resource = new GettextResource(env, path, locale, category, domain);
      _cache.put(key, resource);
    }

    return resource;
  }

  private GettextDomainMap getDomains(Env env)
  {
    Object val = env.getSpecialValue("caucho.gettext_domains");

    if (val == null) {
      val = new GettextDomainMap();

      env.setSpecialValue("caucho.gettext_domains", val);
    }
    
    return (GettextDomainMap) val;
  }
  
  private GettextDomain getDomain(Env env, String name)
  {
    return getDomains(env).getDomain(env, name);
  }
  
  private GettextDomain getCurrentDomain(Env env)
  {
    return getDomains(env).getCurrent(env);
  }

  private void setCurrentDomain(Env env, String name)
  {
    getDomains(env).setCurrent(name);
  }

  /**
   * Gets the name for this category.
   */
  private String getCategory(Env env, int category)
  {
    switch (category) {
      case StringModule.LC_MESSAGES:
        return "LC_MESSAGES";
      case StringModule.LC_ALL:
        return "LC_ALL";
      case StringModule.LC_CTYPE:
        return "LC_CTYPE";
      case StringModule.LC_NUMERIC:
        return "LC_NUMERIC";
      case StringModule.LC_TIME:
        return "LC_TIME";
      case StringModule.LC_COLLATE:
        return "LC_COLLATE";
      case StringModule.LC_MONETARY:
        return "LC_MONETARY";
      default:
        env.warning(L.l("Invalid category. Please use named constants"));
        return "LC_MESSAGES";
    }
  }

  private static StringValue errorReturn(StringValue msgid1,
                              StringValue msgid2,
                              int n)
  {
    if (n == 1)
      return msgid1;
    else
      return msgid2;
  }

  private static StringValue format(Env env,
                              StringValue msg,
                              Value []args)
  {
    if (args.length == 0)
      return msg;
    
    StringValue sb;
    
    if (msg.isUnicode())
      sb = env.createUnicodeBuilder();
    else
      sb = env.createBinaryBuilder();
    
    return formatImpl(env, msg, args, sb);
  }

  /*
  private static StringValue formatBinary(Env env,
                                          StringValue msg,
                                          Value []args,
                                          String charset)
  {
    StringValue sb = env.createBinaryBuilder();

    byte []bytes = null;
    
    try {
      bytes = msg.toString().getBytes(charset);
    }
    catch (UnsupportedEncodingException e) {
      throw new QuercusModuleException(e);
    }
    
    int i = 0;
    int len = bytes.length;
    
    
    while (i < len) {
      byte ch = bytes[i];

      if (ch != '[' || i + 4 > len) {
        sb.appendByte(ch);
        i++;
      }
      else if (bytes[i + 1] != '_') {
        sb.appendByte('[');
        i++;
      }
      else if (bytes[i + 3] != ']') {
        sb.appendByte('[');
        sb.appendByte('_');
        i += 2;
      }
      else {
        ch = bytes[i + 2];
        int argIndex = ch - '0';

        if (0 <= argIndex && argIndex < args.length) {
          sb.append(args[argIndex]);
          i += 4;
        }
        else {
          sb.appendByte('[');
          i++;
        }
      }
    }

    return sb;
  }
  */

  private static StringValue formatImpl(Env env,
                                        StringValue msg,
                                        Value []args,
                                        StringValue sb)
  {
    int i = 0;
    int length = msg.length();

    while (i < length) {
      char ch = msg.charAt(i);

      if (ch != '[' || i + 4 > length) {
        sb.append(ch);
        i++;
      }
      else if (msg.charAt(i + 1) != '_') {
        sb.append(ch);
        i++;
      }
      else if (msg.charAt(i + 3) != ']') {
        sb.append(ch);
        i++;
      }
      else {
        ch = msg.charAt(i + 2);
        int argIndex = ch - '0';

        if (0 <= argIndex && argIndex < args.length) {
          sb.append(args[argIndex]);
          i += 4;
        }
        else {
          sb.append('[');
          i++;
        }
      }
    }
    
    return sb;
  }
}
