/*
 * Copyright 2011 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 java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Helper class for parsing MessageFormat-style format strings.
 */
public class MessageFormatUtils {

  /**
   * Represents an argument in a template string.
   */
  public static class ArgumentChunk extends TemplateChunk {

    private final int argNumber;
    private final String format;
    private final Map<String, String> formatArgs;
    private final String subFormat;
    private final Map<String, String> listArgs;

    public ArgumentChunk(int argNumber, Map<String, String> listArgs,
        String format, Map<String, String> formatArgs, String subformat) {
      this.argNumber = argNumber;
      this.format = format;
      this.subFormat = subformat;
      this.listArgs = listArgs;
      this.formatArgs = formatArgs;
    }

    @Override
    public void accept(TemplateChunkVisitor visitor)
        throws VisitorAbortException {
      visitor.visit(this);
    }

    /**
     * Get the argument number this chunk refers to.
     * 
     * @return the argument number or -1 to refer to the right-most plural
     *     argument
     */
    public int getArgumentNumber() {
      return argNumber;
    }

    public String getFormat() {
      return format;
    }

    public Map<String, String> getFormatArgs() {
      return formatArgs;
    }

    public Map<String, String> getListArgs() {
      return listArgs;
    }

    public String getSubFormat() {
      return subFormat;
    }

    public boolean isList() {
      return listArgs != null;
    }

    @Override
    public String toString() {
      return "Argument: #=" + argNumber + ", format=" + format + ", subformat="
          + subFormat;
    }

    @Override
    protected String getStringValue(boolean quote) {
      StringBuilder buf = new StringBuilder();
      buf.append('{');
      if (argNumber < 0) {
        buf.append('#');
      } else {
        buf.append(argNumber);
      }
      Map<String, String> map = listArgs;
      if (map != null) {
        buf.append(",list");
        appendArgs(buf, map, quote);
      }
      if (format != null || subFormat != null) {
        buf.append(',');
      }
      if (format != null) {
        buf.append(quoteMessageFormatChars(format, quote));
        appendArgs(buf, formatArgs, quote);
      }
      if (subFormat != null) {
        buf.append(',');
        buf.append(subFormat);
      }
      buf.append('}');
      return buf.toString();
    }

    /**
     * @param buf
     * @param map
     * @param quote
     */
    private void appendArgs(
        StringBuilder buf, Map<String, String> map, boolean quote) {
      char prefix = ':';
      for (Map.Entry<String, String> entry : map.entrySet()) {
        String key = entry.getKey();
        if (quote) {
          key = quoteMessageFormatChars(key);
        }
        buf.append(prefix).append(key);
        String value = entry.getValue();
        if (value != null) {
          if (quote) {
            value = quoteMessageFormatChars(value);
          }
          buf.append('=').append(value);
        }
        prefix = ',';
      }
    }
  }

  /**
   * Default implementation of TemplateChunkVisitor -- other implementations
   * should extend this if possible to avoid breakage when new TemplateChunk
   * subtypes are added.
   */
  public static class DefaultTemplateChunkVisitor
      implements TemplateChunkVisitor {

    /**
     * @throws VisitorAbortException  
     */
    public void visit(ArgumentChunk argChunk) throws VisitorAbortException {
    }

    /**
     * @throws VisitorAbortException  
     */
    public void visit(StaticArgChunk staticArgChunk)
        throws VisitorAbortException {
    }

    /**
     * @throws VisitorAbortException  
     */
    public void visit(StringChunk stringChunk)
        throws VisitorAbortException {
    }
  }

  /**
   * Represents the style of quoting used in the source and translated messages.
   */
  public enum MessageStyle {
    
    PLAIN {
      @Override
      public String assemble(List<TemplateChunk> message)
          throws VisitorAbortException {
        final StringBuilder buf = new StringBuilder();
        TemplateChunkVisitor visitor = new DefaultTemplateChunkVisitor() {
          @Override
          public void visit(ArgumentChunk argChunk)
              throws VisitorAbortException {
            throw new VisitorAbortException("Argument in Plain message");
          }

          @Override
          public void visit(StaticArgChunk argChunk)
              throws VisitorAbortException {
            buf.append(argChunk.getString());
          }

          @Override
          public void visit(StringChunk stringChunk)
              throws VisitorAbortException {
           buf.append(stringChunk.getString());
          }
        };
        for (TemplateChunk chunk : message) {
          chunk.accept(visitor);
        }
        return buf.toString();
      }

      @Override
      public List<TemplateChunk> parse(String template) throws ParseException {
        return Arrays.<TemplateChunk>asList(new StringChunk(template));
      }
    },

    MESSAGE_FORMAT {
      @Override
      public String assemble(List<TemplateChunk> message) {
        final StringBuilder buf = new StringBuilder();
        for (TemplateChunk part : message) {
          buf.append(part.getAsMessageFormatString());
        }
        return buf.toString();
      }

      @Override
      public List<TemplateChunk> parse(String template) throws ParseException {
        int curPos = 0;
        boolean inQuote = false;
        int templateLen = template.length();
        ArrayList<TemplateChunk> chunks = new ArrayList<TemplateChunk>();
        TemplateChunk curChunk = null;
        while (curPos < templateLen) {
          char ch = template.charAt(curPos++);
          switch (ch) {
            case '\'':
              if (curPos < templateLen && template.charAt(curPos) == '\'') {
                curChunk = appendString(chunks, curChunk, "'");
                ++curPos;
                break;
              }
              inQuote = !inQuote;
              break;

            case '{':
              if (inQuote) {
                curChunk = appendString(chunks, curChunk, "{");
                break;
              }
              StringBuilder argBuf = new StringBuilder();
              boolean argQuote = false;
              while (curPos < templateLen) {
                ch = template.charAt(curPos++);
                if (ch == '\'') {
                  if (curPos < templateLen && template.charAt(curPos) == '\'') {
                    argBuf.append(ch);
                    ++curPos;
                  } else {
                    argQuote = !argQuote;
                  }
                } else {
                  if (!argQuote && ch == '}') {
                    break;
                  }
                  argBuf.append(ch);
                }
              }
              if (ch != '}') {
                throw new ParseException(
                    "Invalid message format - { not start of valid argument"
                        + template, curPos);
              }
              if (curChunk != null) {
                chunks.add(curChunk);
              }
              String arg = argBuf.toString();
              int firstComma = arg.indexOf(',');
              String format = null;
              if (firstComma > 0) {
                format = arg.substring(firstComma + 1);
                arg = arg.substring(0, firstComma);
              }
              if (!"#".equals(arg) && !Character.isDigit(arg.charAt(0))) {
                // static argument
                chunks.add(new StaticArgChunk(arg, format));
              } else {
                int argNumber = -1;
                if (!"#".equals(arg)) {
                  argNumber = Integer.valueOf(arg);
                }
                Map<String, String> formatArgs = new HashMap<String, String>();
                Map<String, String> listArgs = null;
                String subFormat = null;
                if (format != null) {
                  int comma = format.indexOf(',');
                  if (comma >= 0) {
                    subFormat = format.substring(comma + 1);
                    format = format.substring(0, comma);
                  }
                  format = parseFormatArgs(format, formatArgs);
                  if ("list".equals(format)) {
                    listArgs = formatArgs;
                    formatArgs = new HashMap<String, String>();
                    format = subFormat;
                    subFormat = null;
                    if (format != null) {
                      comma = format.indexOf(',');
                      if (comma >= 0) {
                        subFormat = format.substring(comma + 1);
                        format = format.substring(0, comma);
                      }
                      format = parseFormatArgs(format, formatArgs);
                    }
                  }
                }
                chunks.add(new ArgumentChunk(
                    argNumber, listArgs, format, formatArgs, subFormat));
              }
              curChunk = null;
              break;

            case '\n':
              curChunk = appendString(chunks, curChunk, "\\n");
              break;

            case '\r':
              curChunk = appendString(chunks, curChunk, "\\r");
              break;

            case '\\':
              curChunk = appendString(chunks, curChunk, "\\\\");
              break;

            case '"':
              curChunk = appendString(chunks, curChunk, "\\\"");
              break;

            default:
              curChunk = appendString(chunks, curChunk, String.valueOf(ch));
              break;
          }
        }
        if (inQuote) {
          throw new ParseException(
              "Unterminated single quote: " + template, template.length());
        }
        if (curChunk != null) {
          chunks.add(curChunk);
        }
        return chunks;
      }
    };

    /**
     * Assemble a message from its parsed chunks.
     * 
     * @param message
     * @return assembled message
     * @throws VisitorAbortException 
     */
    public abstract String assemble(List<TemplateChunk> message)
        throws VisitorAbortException;

    /**
     * Parse a message into chunks.
     * 
     * @param template
     * @return a list of chunks
     * @throws ParseException
     */
    public abstract List<TemplateChunk> parse(String template)
        throws ParseException;

    /**
     * Parse a message template and call the supplied visitor on each parsed
     * chunk.
     * 
     * @param template
     * @param visitor
     * @throws ParseException
     * @throws VisitorAbortException
     */
    public void parseAccept(String template, TemplateChunkVisitor visitor)
        throws ParseException, VisitorAbortException {
      for (TemplateChunk chunk : parse(template)) {
        chunk.accept(visitor);
      }
    }
  }

  /**
   * Represents a static argument, which is used to remove markup from
   * translator view without having to supply it at each callsite.
   */
  public static class StaticArgChunk extends TemplateChunk {

    private final String argName;
    private final String replacement;

    public StaticArgChunk(String argName, String replacement) {
      this.argName = argName;
      this.replacement = replacement;
    }

    @Override
    public void accept(TemplateChunkVisitor visitor)
        throws VisitorAbortException {
      visitor.visit(this);
    }

    public String getArgName() {
      return argName;
    }

    public String getReplacement() {
      return replacement;
    }

    @Override
    protected String getStringValue(boolean quoted) {
      StringBuilder buf = new StringBuilder();
      buf.append('{').append(argName);
      if (replacement != null) {
        buf.append(',').append(quoteMessageFormatChars(replacement, quoted));
      }
      buf.append('}');
      return buf.toString();
    }
  }

  /**
   * Represents a literal string portion of a template string.
   */
  public static class StringChunk extends TemplateChunk {

    private StringBuilder buf = new StringBuilder();

    public StringChunk() {
    }

    public StringChunk(String str) {
      buf.append(str);
    }

    @Override
    public void accept(TemplateChunkVisitor visitor)
        throws VisitorAbortException {
      visitor.visit(this);
    }

    public void append(String str) {
      buf.append(str);
    }

    @Override
    public boolean isLiteral() {
      return true;
    }

    @Override
    public String toString() {
      return "StringLiteral: \"" + buf.toString() + "\"";
    }

    @Override
    protected String getStringValue(boolean quote) {
      String str = buf.toString();
      return quoteMessageFormatChars(str, quote);
    }
  }

  /**
   * Represents a parsed chunk of a template.
   */
  public abstract static class TemplateChunk {

    /**
     * Quote a string in the MessageFormat-style.
     *
     * @param str string to quote, must not be null
     * @return quoted string
     */
    protected static String quoteMessageFormatChars(String str) {
      str = str.replace("'", "''");
      str = str.replace("{", "'{'");
      str = str.replace("}", "'}'");
      return str;
    }

    /**
     * Possibly quote a string in the MessageFormat-style.
     *
     * @param str
     * @return quoted string
     */
    protected static String quoteMessageFormatChars(String str, boolean quote) {
      return quote ? quoteMessageFormatChars(str) : str;
    }

    public abstract void accept(TemplateChunkVisitor visitor)
        throws VisitorAbortException;

    /**
     * Returns the string as this chunk would be represented in a MessageFormat
     * template, with any required quoting such that reparsing this value would
     * produce an equivalent (note, not identical) parse.
     *
     * Note that the default implementation may not be sufficient for all
     * subclasses.
     */
    public String getAsMessageFormatString() {
      return getStringValue(true);
    }

    /**
     * Returns the string as this chunk would be represented in a MessageFormat
     * template, with any quoting removed. Note that this is distinct from
     * toString in that the latter is intend for human consumption.
     */
    public String getString() {
      return getStringValue(false);
    }

    public boolean isLiteral() {
      return false;
    }

    /**
     * Returns the optionally quoted string value of this chunk as represented
     * in a MessgeFormat string.
     *
     * @param quote true if the result should be quoted
     * @return optionally quoted MessageFormat string
     */
    protected abstract String getStringValue(boolean quote);
  }

  /**
   * Visitor for template chunks.
   */
  public interface TemplateChunkVisitor {
    void visit(ArgumentChunk argChunk) throws VisitorAbortException;

    void visit(StaticArgChunk staticArgChunk) throws VisitorAbortException;

    void visit(StringChunk stringChunk) throws VisitorAbortException;
  }

  /**
   * An exception thrown to indicate a visitor wishes to abort processing a
   * message.
   */
  public static class VisitorAbortException extends Exception {

    /**
     * Initialize with a required reason.
     * 
     * @param reason human-readable explanation of why the abort occurred
     */
    public VisitorAbortException(String reason) {
      super(reason);
    }

    /**
     * Initialize with a required reason.
     * 
     * @param reason human-readable explanation of why the abort occurred
     * @param cause related exception
     */
    public VisitorAbortException(String reason, Throwable cause) {
      super(reason, cause);
    }

    /**
     * Initialize with a required reason.
     * 
     * @param cause related exception
     */
    public VisitorAbortException(Throwable cause) {
      super(cause.getMessage(), cause);
    }
  }

  private static TemplateChunk appendString(ArrayList<TemplateChunk> chunks,
      TemplateChunk curChunk, String string) {
    if (curChunk != null && !curChunk.isLiteral()) {
      chunks.add(curChunk);
      curChunk = null;
    }
    if (curChunk == null) {
      curChunk = new StringChunk(string);
    } else {
      ((StringChunk) curChunk).append(string);
    }
    return curChunk;
  }

  /**
   * Parse any arguments appended to a format. The syntax is:
   * format[:tag[=value][:tag[=value]]... for example: "date:tz=EST:showoffset"
   *
   * @param format format value to parse
   * @param formatArgs map to add tag/value pairs to
   * @return format portion of supplied string
   */
  private static String parseFormatArgs(String format,
      Map<String, String> formatArgs) {
    int colon = format.indexOf(':');
    if (colon >= 0) {
      for (String tagValue : format.substring(colon + 1).split(":")) {
        int equals = tagValue.indexOf('=');
        String value = "";
        if (equals >= 0) {
          value = tagValue.substring(equals + 1).trim();
          tagValue = tagValue.substring(0, equals);
        }
        formatArgs.put(tagValue.trim(), value);
      }
      format = format.substring(0, colon);
    }
    return format;
  }
}
