/*
 * 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.apache.log4j.pattern;

import java.util.ArrayList;
import java.util.List;


/**
 * NameAbbreviator generates abbreviated logger and class names.
 *
 */
public abstract class NameAbbreviator {
  /**
   * Default (no abbreviation) abbreviator.
   */
  private static final NameAbbreviator DEFAULT = new NOPAbbreviator();

  /**
   * Gets an abbreviator.
   *
   * For example, "%logger{2}" will output only 2 elements of the logger name,
   * %logger{-2} will drop 2 elements from the logger name,
   * "%logger{1.}" will output only the first character of the non-final elements in the name,
   * "%logger{1~.2~} will output the first character of the first element, two characters of
   * the second and subsequent elements and will use a tilde to indicate abbreviated characters.
   *
   * @param pattern abbreviation pattern.
   * @return abbreviator, will not be null.
   */
  public static NameAbbreviator getAbbreviator(final String pattern) {
    if (pattern.length() > 0) {
      //  if pattern is just spaces and numbers then
      //     use MaxElementAbbreviator
      String trimmed = pattern.trim();

      if (trimmed.length() == 0) {
        return DEFAULT;
      }

      int i = 0;
      if (trimmed.length() > 0) {
          if (trimmed.charAt(0) == '-') {
              i++;
          }
          for (;
                (i < trimmed.length()) &&
                  (trimmed.charAt(i) >= '0') &&
                  (trimmed.charAt(i) <= '9');
               i++) {
          }
      }


      //
      //  if all blanks and digits
      //
      if (i == trimmed.length()) {
        int elements = Integer.parseInt(trimmed);
        if (elements >= 0) {
            return new MaxElementAbbreviator(elements);
        } else {
            return new DropElementAbbreviator(-elements);
        }
      }

      ArrayList fragments = new ArrayList(5);
      char ellipsis;
      int charCount;
      int pos = 0;

      while ((pos < trimmed.length()) && (pos >= 0)) {
        int ellipsisPos = pos;

        if (trimmed.charAt(pos) == '*') {
          charCount = Integer.MAX_VALUE;
          ellipsisPos++;
        } else {
          if ((trimmed.charAt(pos) >= '0') && (trimmed.charAt(pos) <= '9')) {
            charCount = trimmed.charAt(pos) - '0';
            ellipsisPos++;
          } else {
            charCount = 0;
          }
        }

        ellipsis = '\0';

        if (ellipsisPos < trimmed.length()) {
          ellipsis = trimmed.charAt(ellipsisPos);

          if (ellipsis == '.') {
            ellipsis = '\0';
          }
        }

        fragments.add(new PatternAbbreviatorFragment(charCount, ellipsis));
        pos = trimmed.indexOf(".", pos);

        if (pos == -1) {
          break;
        }

        pos++;
      }

      return new PatternAbbreviator(fragments);
    }

    //
    //  no matching abbreviation, return defaultAbbreviator
    //
    return DEFAULT;
  }

  /**
   * Gets default abbreviator.
   *
   * @return default abbreviator.
   */
  public static NameAbbreviator getDefaultAbbreviator() {
    return DEFAULT;
  }

  /**
   * Abbreviates a name in a StringBuffer.
   *
   * @param nameStart starting position of name in buf.
   * @param buf buffer, may not be null.
   */
  public abstract void abbreviate(final int nameStart, final StringBuffer buf);

  /**
   * Abbreviator that simply appends full name to buffer.
   */
  private static class NOPAbbreviator extends NameAbbreviator {
    /**
     * Constructor.
     */
    public NOPAbbreviator() {
    }

    /**
     * {@inheritDoc}
     */
    public void abbreviate(final int nameStart, final StringBuffer buf) {
    }
  }

  /**
   * Abbreviator that drops starting path elements.
   */
  private static class MaxElementAbbreviator extends NameAbbreviator {
    /**
     * Maximum number of path elements to output.
     */
    private final int count;

    /**
     * Create new instance.
     * @param count maximum number of path elements to output.
     */
    public MaxElementAbbreviator(final int count) {
      this.count = count;
    }

    /**
     * Abbreviate name.
     * @param buf buffer to append abbreviation.
     * @param nameStart start of name to abbreviate.
     */
    public void abbreviate(final int nameStart, final StringBuffer buf) {
      // We substract 1 from 'len' when assigning to 'end' to avoid out of
      // bounds exception in return r.substring(end+1, len). This can happen if
      // precision is 1 and the category name ends with a dot.
      int end = buf.length() - 1;

      String bufString = buf.toString();
      for (int i = count; i > 0; i--) {
        end = bufString.lastIndexOf(".", end - 1);

        if ((end == -1) || (end < nameStart)) {
          return;
        }
      }

      buf.delete(nameStart, end + 1);
    }
  }

  /**
   * Abbreviator that drops starting path elements.
   */
  private static class DropElementAbbreviator extends NameAbbreviator {
    /**
     * Maximum number of path elements to output.
     */
    private final int count;

    /**
     * Create new instance.
     * @param count maximum number of path elements to output.
     */
    public DropElementAbbreviator(final int count) {
      this.count = count;
    }

    /**
     * Abbreviate name.
     * @param buf buffer to append abbreviation.
     * @param nameStart start of name to abbreviate.
     */
    public void abbreviate(final int nameStart, final StringBuffer buf) {
      int i = count;
      for(int pos = buf.indexOf(".", nameStart);
        pos != -1;
        pos = buf.indexOf(".", pos + 1)) {
          if(--i == 0) {
              buf.delete(nameStart, pos + 1);
              break;
          }
      }
    }
  }


  /**
   * Fragment of an pattern abbreviator.
   *
   */
  private static class PatternAbbreviatorFragment {
    /**
     * Count of initial characters of element to output.
     */
    private final int charCount;

    /**
     *  Character used to represent dropped characters.
     * '\0' indicates no representation of dropped characters.
     */
    private final char ellipsis;

    /**
     * Creates a PatternAbbreviatorFragment.
     * @param charCount number of initial characters to preserve.
     * @param ellipsis character to represent elimination of characters,
     *    '\0' if no ellipsis is desired.
     */
    public PatternAbbreviatorFragment(
      final int charCount, final char ellipsis) {
      this.charCount = charCount;
      this.ellipsis = ellipsis;
    }

    /**
     * Abbreviate element of name.
     * @param buf buffer to receive element.
     * @param startPos starting index of name element.
     * @return starting index of next element.
     */
    public int abbreviate(final StringBuffer buf, final int startPos) {
      int nextDot = buf.toString().indexOf(".", startPos);

      if (nextDot != -1) {
        if ((nextDot - startPos) > charCount) {
          buf.delete(startPos + charCount, nextDot);
          nextDot = startPos + charCount;

          if (ellipsis != '\0') {
            buf.insert(nextDot, ellipsis);
            nextDot++;
          }
        }

        nextDot++;
      }

      return nextDot;
    }
  }

  /**
   * Pattern abbreviator.
   *
   *
   */
  private static class PatternAbbreviator extends NameAbbreviator {
    /**
     * Element abbreviation patterns.
     */
    private final PatternAbbreviatorFragment[] fragments;

    /**
     * Create PatternAbbreviator.
     *
     * @param fragments element abbreviation patterns.
     */
    public PatternAbbreviator(List fragments) {
      if (fragments.size() == 0) {
        throw new IllegalArgumentException(
          "fragments must have at least one element");
      }

      this.fragments = new PatternAbbreviatorFragment[fragments.size()];
      fragments.toArray(this.fragments);
    }

    /**
     * Abbreviate name.
     * @param buf buffer that abbreviated name is appended.
     * @param nameStart start of name.
     */
    public void abbreviate(final int nameStart, final StringBuffer buf) {
      //
      //  all non-terminal patterns are executed once
      //
      int pos = nameStart;

      for (int i = 0; (i < (fragments.length - 1)) && (pos < buf.length());
          i++) {
        pos = fragments[i].abbreviate(buf, pos);
      }

      //
      //   last pattern in executed repeatedly
      //
      PatternAbbreviatorFragment terminalFragment =
        fragments[fragments.length - 1];

      while ((pos < buf.length()) && (pos >= 0)) {
        pos = terminalFragment.abbreviate(buf, pos);
      }
    }
  }
}
