/*
 * Copyright 2002-2010 the original author or authors.
 * 
 * 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 de.ivu.elect.jee.faces.messagehelper;

import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;


public class MessageSourceSupport implements Serializable {

  private static final long serialVersionUID = 1L;

  private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");

  private boolean alwaysUseMessageFormat = false;

  private final ConcurrentHashMap<Locale, ConcurrentHashMap<String, MessageFormat>> messageFormats = new ConcurrentHashMap<>();

  /**
   * Set whether to always apply the MessageFormat rules, parsing even messages without arguments.
   * <p/>
   * Default is "false": Messages without arguments are by default returned as-is, without parsing them through
   * MessageFormat. Set this to "true" to enforce MessageFormat for all messages, expecting all message texts to be
   * written with MessageFormat escaping.
   * <p/>
   * For example, MessageFormat expects a single quote to be escaped as "''". If your message texts are all written with
   * such escaping, even when not defining argument placeholders, you need to set this flag to "true". Else, only
   * message texts with actual arguments are supposed to be written with MessageFormat escaping.
   *
   * @see java.text.MessageFormat
   */
  public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {
    this.alwaysUseMessageFormat = alwaysUseMessageFormat;
  }

  /**
   * Return whether to always apply the MessageFormat rules, parsing even messages without arguments.
   */
  protected boolean isAlwaysUseMessageFormat() {
    return this.alwaysUseMessageFormat;
  }

  /**
   * Render the given default message String. The default message is passed in as specified by the caller and can be
   * rendered into a fully formatted default message shown to the user.
   * <p/>
   * The default implementation passes the String to <code>formatMessage</code>, resolving any argument placeholders
   * found in them. Subclasses may override this method to plug in custom processing of default messages.
   *
   * @param defaultMessage the passed-in default message String
   * @param args           array of arguments that will be filled in for params within the message, or <code>null</code> if none.
   * @param locale         the Locale used for formatting
   * @return the rendered default message (with resolved arguments)
   * @see #formatMessage(String, Object[], java.util.Locale)
   */
  protected String renderDefaultMessage(@Nullable String defaultMessage, Object[] args, Locale locale) {
    return formatMessage(defaultMessage, args, locale);
  }

  /**
   * Format the given message String, using cached MessageFormats. By default invoked for passed-in default messages, to
   * resolve any argument placeholders found in them.
   *
   * @param msg    the message to format
   * @param args   array of arguments that will be filled in for params within the message, or <code>null</code> if none
   * @param locale the Locale used for formatting
   * @return the formatted message (with resolved arguments)
   */
  protected String formatMessage(String msg, Object[] args, Locale locale) {
    if (msg == null || !this.alwaysUseMessageFormat && isEmpty(args)) {
      return msg;
    }

    ConcurrentHashMap<String, MessageFormat> formatMap = this.messageFormats.get(locale);
    if (formatMap == null) {
      ConcurrentHashMap<String, MessageFormat> newFormatMap = new ConcurrentHashMap<>();
      formatMap = messageFormats.putIfAbsent(locale, newFormatMap);
      if (formatMap == null) {
        formatMap = newFormatMap;
      }
    }

    MessageFormat messageFormat = formatMap.get(msg);
    if (messageFormat == null) {
      MessageFormat newMessageFormat;
      try {
        newMessageFormat = createMessageFormat(msg, locale);
      } catch (IllegalArgumentException ex) {
        // invalid message format - probably not intended for formatting,
        // rather using a message structure with no arguments involved
        if (this.alwaysUseMessageFormat) {
          throw ex;
        }
        // silently proceed with raw message if format not enforced
        newMessageFormat = INVALID_MESSAGE_FORMAT;
      }
      messageFormat = formatMap.putIfAbsent(msg, newMessageFormat);
      if (messageFormat == null) {
        messageFormat = newMessageFormat;
      }
    }

    if (INVALID_MESSAGE_FORMAT.equals(messageFormat)) {
      return msg;
    }

    synchronized (messageFormat) {
      return messageFormat.format(resolveArguments(args, locale));
    }
  }

  protected boolean isEmpty(@Nullable Object[] args) {
    return args == null || args.length == 0;
  }

  /**
   * Create a MessageFormat for the given message and Locale.
   *
   * @param msg    the message to create a MessageFormat for
   * @param locale the Locale to create a MessageFormat for
   * @return the MessageFormat instance
   */
  protected MessageFormat createMessageFormat(String msg, Locale locale) {
    return new MessageFormat(msg == null ? "" : msg, locale);
  }

  /**
   * Template method for resolving argument objects.
   * <p/>
   * The default implementation simply returns the given argument array as-is. Can be overridden in subclasses in order
   * to resolve special argument types.
   *
   * @param args   the original argument array
   * @param locale the Locale to resolve against
   * @return the resolved argument array
   */
  protected Object[] resolveArguments(Object[] args, Locale locale) {
    return args;
  }

}
