/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed 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 com.google.gwt.i18n.server;

import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * Creates server-side GwtLocale instances.  Thread-safe.
 */
public class GwtLocaleFactoryImpl implements GwtLocaleFactory {

  private static boolean isAlpha(String str, int min, int max) {
    return matches(str, min, max, true);
  }

  private static boolean isDigit(String str, int min, int max) {
    return matches(str, min, max, false);
  }

  /**
   * Check if the supplied string matches length and composition requirements.
   * 
   * @param str string to check
   * @param min minimum length
   * @param max maximum length
   * @param lettersNotDigits true if all characters should be letters, false if
   *     all characters should be digits
   * @return true if the string is of a proper length and contains only the
   *     specified characters 
   */
  private static boolean matches(String str, int min, int max,
      boolean lettersNotDigits) {
    int len = str.length();
    if (len < min || len > max) {
      return false;
    }
    for (int i = 0; i < len; ++i) {
      if ((lettersNotDigits && !Character.isLetter(str.charAt(i)))
          || (!lettersNotDigits && !Character.isDigit(str.charAt(i)))) {
        return false;
      }
    }
    return true;
  }

  private static String titleCase(String str) {
    if (str.length() < 2) {
      return str.toUpperCase(Locale.ROOT);
    }
    return String.valueOf(Character.toTitleCase(str.charAt(0))) +
        str.substring(1).toLowerCase(Locale.ROOT);
  }

  private final Object instanceCacheLock = new Object[0];
  
  // Locales are stored pointing at themselves. A new instance is created,
  // which is pretty cheap, then looked up here. If it exists, the old
  // one is used instead to preserved cached data structures.
  private Map<GwtLocaleImpl, GwtLocaleImpl> instanceCache
      = new HashMap<GwtLocaleImpl, GwtLocaleImpl>();

  /**
   * Clear an embedded cache of instances when they are no longer needed.
   * <p>
   * Note that GwtLocale instances constructed after this is called will not
   * maintain identity with instances constructed before this call.
   */
  public void clear() {
    synchronized (instanceCacheLock) {
      instanceCache.clear();     
    }
  }

  public GwtLocale fromComponents(String language, String script,
      String region, String variant) {
    if (language != null && language.length() == 0) {
      language = null;
    }
    if (language != null) {
      language = language.toLowerCase(Locale.ROOT);
    }
    if (script != null && script.length() == 0) {
      script = null;
    }
    if (script != null) {
      script = titleCase(script);
    }
    if (region != null && region.length() == 0) {
      region = null;
    }
    if (region != null) {
      region = region.toUpperCase(Locale.ROOT);
    }
    if (variant != null && variant.length() == 0) {
      variant = null;
    }
    if (variant != null) {
      variant = variant.toUpperCase(Locale.ROOT);
    }
    GwtLocaleImpl locale = new GwtLocaleImpl(this, language, region, script,
        variant);
    synchronized (instanceCacheLock) {
      if (instanceCache.containsKey(locale)) {
        return instanceCache.get(locale);
      }
      instanceCache.put(locale, locale);
    }
    return locale;
  }

  /**
   * @throws IllegalArgumentException if the supplied locale does not match
   *     BCP47 structural requirements.
   */
  public GwtLocale fromString(String localeName) {
    String language = null;
    String script = null;
    String region = null;
    String variant = null;
    if (localeName != null && !GwtLocale.DEFAULT_LOCALE.equals(localeName)) {
      // split into component parts
      ArrayList<String> localeParts = new ArrayList<String>();
      String[] parts = localeName.split("[-_]");
      for (int i = 0; i < parts.length; ++i) {
        if (parts[i].length() == 1 && i + 1 < parts.length) {
          localeParts.add(parts[i] + '-' + parts[++i]);
        } else {
          localeParts.add(parts[i]);
        }
      }
      
      // figure out the role of each part
      int partIdx = 1;
      int numParts = localeParts.size();
      if (numParts > 0) {
        // Treat an initial private-use tag as the language tag
        // Otherwise, language tags are 2-3 characters plus up to three
        // 3-letter extensions, or 4-8 characters.
        language = localeParts.get(0);
        int len = language.length();
        // TODO: verify language tag length
        // See if we have extended language tags
        if ((len == 2 || len == 3) && partIdx < numParts) {
          String part = localeParts.get(partIdx);
          while (partIdx < numParts && partIdx < 4
              && isAlpha(part, 3, 3)) {
            language += '-' + part;
            if (++partIdx >= numParts) {
              break;
            }
            part = localeParts.get(partIdx);
          }
        }
      }
      if (numParts > partIdx
          && isAlpha(localeParts.get(partIdx), 4, 4)) {
        // Scripts are exactly 4 letters
        script = localeParts.get(partIdx++);
      }
      if (partIdx < numParts) {
        // Regions may be 2 letters or 3 digits
        String part = localeParts.get(partIdx);
        if (isAlpha(part, 2, 2) || isDigit(part, 3, 3)) {
          region = part;
          ++partIdx;
        }
      }
      if (partIdx < numParts) {
        // Variants are 5-8 alphanum, or 4 alphanum if first is digit
        String part = localeParts.get(partIdx);
        int len = part.length();
        if ((len >= 5 && len <= 8) || (len == 4
            && Character.isDigit(part.charAt(0)))) {
          variant = part;
          ++partIdx;
        }
      }
      if (partIdx < numParts) {
        throw new IllegalArgumentException("Unrecognized locale format: "
            + localeName);
      }
    }
    return fromComponents(language, script, region, variant);
  }

  public GwtLocale getDefault() {
    return fromComponents(null, null, null, null);
  }
}
