/*
 * 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.ipc.invalidation.external.client.android.service;

import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.util.Formatter;

import android.util.Log;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;


/**
 * Provides the implementation of {@link Logger} for Android. The logging tag will be based upon the
 * top-level class name containing the code invoking the logger (the outer class, not an inner or
 * anonymous class name).   For severe and warning level messages, the Android logger will also
 * dump the stack trace of the first argument if it is a throwable.
 */
public class AndroidLogger implements Logger {

  /** Creates a new AndroidLogger that uses the provided value as the Android logging tag */
  public static AndroidLogger forTag(String tag) {
    return new AndroidLogger(tag, null);
  }

  /** Creates a new AndroidLogger that will compute a tag value dynamically based upon the class
   * that calls into the logger and will prepend the provided prefix (if any) on all
   * logged messages.
   */
  public static AndroidLogger forPrefix(String prefix) {
    return new AndroidLogger(null, prefix);
  }

  /**
   * If {@code false}, then Log.isLoggable() is called to filter log messages
   */
  private static boolean filteringDisabled = false;

  /**
   * Maps from a Java {@link Level} to the android {@link Log} priority value used to log
   * messages at that level.
   */
  private static Map<Level, Integer> levelToPriority = new HashMap<Level, Integer>();

  static {
    // Define the mappings for Java log levels to the associated Android log priorities
    levelToPriority.put(Level.INFO, Log.INFO);
    levelToPriority.put(Level.WARNING, Log.WARN);
    levelToPriority.put(Level.SEVERE, Log.ERROR);
    levelToPriority.put(Level.FINE, Log.DEBUG);
    levelToPriority.put(Level.FINER, Log.VERBOSE);
    levelToPriority.put(Level.FINEST, Log.VERBOSE);
    levelToPriority.put(Level.CONFIG, Log.INFO);
  }

  /**
   * Disables log filtering so all logged messages will be captured.
   */
  public static void disableFilteringForTest() {
    filteringDisabled = true;
  }

  /**
   * The default minimum Android log level. We default to 0 to ensure everything is logged.
   * This should be a value from the {@link Log} constants.
   */
  private static int minimumLogLevel = 0;

  /**
   * The maximum length of an Android logging tag. There's no formal constants but the constraint is
   * mentioned in the Log javadoc
   */
  private static final int MAX_TAG_LENGTH = 23;

  /** Constant tag to use for logged messages (or {@code null} to use topmost class on stack */
  private final String tag;

  /** Prefix added to Android logging messages */
  private final String logPrefix;

  /** Creates a logger that prefixes every logging stmt with {@code logPrefix}. */
  private AndroidLogger(String tag, String logPrefix) {
    this.tag = tag;
    this.logPrefix = logPrefix;
  }

  @Override
  public boolean isLoggable(Level level) {
    return isLoggable(getTag(), levelToPriority(level));
  }

  @Override
  public void log(Level level, String template, Object... args) {
    int androidLevel = levelToPriority(level);
    String tag = getTag();
    if (isLoggable(tag, androidLevel)) {
      Log.println(androidLevel, tag, format(template, args));
    }
  }

  @Override
  public void severe(String template, Object...args) {
    String tag = getTag();
    if (isLoggable(tag, Log.ERROR)) {
      // If the first argument is an exception, use the form of Log that will dump a stack trace
      if ((args.length > 0) && (args[0] instanceof Throwable)) {
        Log.e(tag, format(template, args), (Throwable) args[0]);
      } else {
        Log.e(tag, format(template, args));
      }
    }
  }

  @Override
  public void warning(String template, Object...args) {
    String tag = getTag();
    if (isLoggable(tag, Log.WARN)){
      // If the first argument is an exception, use the form of Log that will dump a stack trace
      if ((args.length > 0) && (args[0] instanceof Throwable)) {
        Log.w(tag, format(template, args), (Throwable) args[0]);
      } else {
        Log.w(tag, format(template, args));
      }
    }
  }

  @Override
  public void info(String template, Object...args) {
    String tag = getTag();
    if (isLoggable(tag, Log.INFO)) {
      Log.i(tag, format(template, args));
    }
  }

  @Override
  public void fine(String template, Object...args) {
    String tag = getTag();
    if (isLoggable(tag, Log.DEBUG)) {
      Log.d(tag, format(template, args));
    }
  }

  @Override
  public void setSystemResources(SystemResources resources) {
    // No-op.
  }

  /** Given a Java logging level, returns the corresponding Android log priority. */
  private static int levelToPriority(Level level) {
    Integer priority = levelToPriority.get(level);
    if (priority != null) {
      return priority;
    }
    throw new IllegalArgumentException("Unsupported level: " + level);
  }

  /** Formats the content of a logged messages for output, prepending the log prefix if any. */
  private String format(String template, Object...args) {
    return (logPrefix != null) ?
        ("[" + logPrefix + "] " + Formatter.format(template, args)) :
        Formatter.format(template, args);
  }

  /** Returns the Android logging tag that should be placed on logged messages */
  private String getTag() {
    if (tag != null) {
      return tag;
    }

    StackTraceElement[] stackTrace = new Throwable().getStackTrace();
    String className = null;
    for (int i = 0; i < stackTrace.length; i++) {
      className = stackTrace[i].getClassName();

      // Skip over this class's methods
      if (!className.equals(AndroidLogger.class.getName())) {
        break;
      }
    }

    // Compute the unqualified class name w/out any inner class, then truncate to the
    // maximum tag length.
    int unqualBegin = className.lastIndexOf('.') + 1;
    if (unqualBegin < 0) { // should never happen, but be safe
      unqualBegin = 0;
    }
    int unqualEnd = className.indexOf('$', unqualBegin);
    if (unqualEnd < 0) {
      unqualEnd = className.length();
    }
    if ((unqualEnd - unqualBegin) > MAX_TAG_LENGTH) {
      unqualEnd = unqualBegin + MAX_TAG_LENGTH;
    }
    return className.substring(unqualBegin, unqualEnd);
  }

  /**
   * Add additional constraint on logging. In addition to the normal check of
   * {@link Log#isLoggable(String, int)} for logging, this also requires a minimum
   * log level of the given value. This should be a value from the {@link Log} constants.
   */
  public static void setMinimumAndroidLogLevel(int logLevel) {
    minimumLogLevel = logLevel;
  }

  /**
   * Returns {@code true} is the provided tag/level will produce logged output.
   */
  
  boolean isLoggable(String tag, int priority) {
    return filteringDisabled || (priority >= minimumLogLevel && Log.isLoggable(tag, priority));
  }
}