/*
 * 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.flume.agent.embedded;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.flume.FlumeException;
import org.apache.flume.annotations.InterfaceAudience;
import org.apache.flume.annotations.InterfaceStability;
import org.apache.flume.conf.BasicConfigurationConstants;
import org.apache.flume.conf.channel.ChannelType;
import org.apache.flume.conf.sink.SinkProcessorType;
import org.apache.flume.conf.sink.SinkType;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;

/**
 * Stores publicly accessible configuration constants and private
 * configuration constants and methods.
 */
@InterfaceAudience.Public
@InterfaceStability.Stable
public class EmbeddedAgentConfiguration {
  public static final String SEPERATOR = ".";
  private static final Joiner JOINER = Joiner.on(SEPERATOR);
  private static final String TYPE = "type";

  /**
   * Prefix for source properties
   */
  public static final String SOURCE = "source";
  /**
   * Prefix for channel properties
   */
  public static final String CHANNEL = "channel";
  /**
   * Prefix for sink processor properties
   */
  public static final String SINK_PROCESSOR = "processor";
  /**
   * Space delimited list of sink names: e.g. sink1 sink2 sink3
   */
  public static final String SINKS = "sinks";

  public static final String SINKS_PREFIX = join(SINKS, "");
  /**
   * Source type, choices are `embedded'
   */
  public static final String SOURCE_TYPE = join(SOURCE, TYPE);
  /**
   * Prefix for passing configuration parameters to the source
   */
  public static final String SOURCE_PREFIX = join(SOURCE, "");
  /**
   * Channel type, choices are `memory' or `file'
   */
  public static final String CHANNEL_TYPE = join(CHANNEL, TYPE);
  /**
   * Prefix for passing configuration parameters to the channel
   */
  public static final String CHANNEL_PREFIX = join(CHANNEL, "");
  /**
   * Sink processor type, choices are `default', `failover' or `load_balance'
   */
  public static final String SINK_PROCESSOR_TYPE = join(SINK_PROCESSOR, TYPE);
  /**
   * Prefix for passing configuration parameters to the sink processor
   */
  public static final String SINK_PROCESSOR_PREFIX = join(SINK_PROCESSOR, "");
  /**
   * Embedded source which provides simple in-memory transfer to channel.
   * Use this source via the put,putAll methods on the EmbeddedAgent. This
   * is the only supported source to use for Embedded Agents.
   */
  public static final String SOURCE_TYPE_EMBEDDED = EmbeddedSource.class.getName();
  private static final String SOURCE_TYPE_EMBEDDED_ALIAS = "EMBEDDED";
  /**
   * Memory channel which stores events in heap. See Flume User Guide for
   * configuration information. This is the recommended channel to use for
   * Embedded Agents.
   */
  public static final String CHANNEL_TYPE_MEMORY = ChannelType.MEMORY.name();

  /**
  * Spillable Memory channel which stores events in heap. See Flume User Guide for
  * configuration information. This is the recommended channel to use for
  * Embedded Agents.
   */
  public static final String CHANNEL_TYPE_SPILLABLEMEMORY = ChannelType.SPILLABLEMEMORY.name();

  /**
   * File based channel which stores events in on local disk. See Flume User
   * Guide for configuration information.
   */
  public static final String CHANNEL_TYPE_FILE = ChannelType.FILE.name();

  /**
   * Avro sink which can send events to a downstream avro source. This is the
   * only supported sink for Embedded Agents.
   */
  public static final String SINK_TYPE_AVRO = SinkType.AVRO.name();

  /**
   * Default sink processors which may be used when there is only a single sink.
   */
  public static final String SINK_PROCESSOR_TYPE_DEFAULT = SinkProcessorType.DEFAULT.name();
  /**
   * Failover sink processor. See Flume User Guide for configuration
   * information.
   */
  public static final String SINK_PROCESSOR_TYPE_FAILOVER = SinkProcessorType.FAILOVER.name();
  /**
   * Load balancing sink processor. See Flume User Guide for configuration
   * information.
   */
  public static final String SINK_PROCESSOR_TYPE_LOAD_BALANCE =
      SinkProcessorType.LOAD_BALANCE.name();

  private static final String[] ALLOWED_SOURCES = {
    SOURCE_TYPE_EMBEDDED_ALIAS,
    SOURCE_TYPE_EMBEDDED,
  };

  private static final String[] ALLOWED_CHANNELS = {
    CHANNEL_TYPE_MEMORY,
    CHANNEL_TYPE_FILE
  };

  private static final String[] ALLOWED_SINKS = {
    SINK_TYPE_AVRO
  };

  private static final String[] ALLOWED_SINK_PROCESSORS = {
    SINK_PROCESSOR_TYPE_DEFAULT,
    SINK_PROCESSOR_TYPE_FAILOVER,
    SINK_PROCESSOR_TYPE_LOAD_BALANCE
  };

  private static final ImmutableList<String> DISALLOWED_SINK_NAMES =
      ImmutableList.of("source", "channel", "processor");

  private static void validate(String name,
      Map<String, String> properties) throws FlumeException {

    if (properties.containsKey(SOURCE_TYPE)) {
      checkAllowed(ALLOWED_SOURCES, properties.get(SOURCE_TYPE));
    }
    checkRequired(properties, CHANNEL_TYPE);
    checkAllowed(ALLOWED_CHANNELS, properties.get(CHANNEL_TYPE));
    checkRequired(properties, SINKS);
    String sinkNames = properties.get(SINKS);
    for (String sink : sinkNames.split("\\s+")) {
      if (DISALLOWED_SINK_NAMES.contains(sink.toLowerCase(Locale.ENGLISH))) {
        throw new FlumeException("Sink name " + sink + " is one of the" +
            " disallowed sink names: " + DISALLOWED_SINK_NAMES);
      }
      String key = join(sink, TYPE);
      checkRequired(properties, key);
      checkAllowed(ALLOWED_SINKS, properties.get(key));
    }
    checkRequired(properties, SINK_PROCESSOR_TYPE);
    checkAllowed(ALLOWED_SINK_PROCESSORS, properties.get(SINK_PROCESSOR_TYPE));
  }
  /**
   * Folds embedded configuration structure into an agent configuration.
   * Should only be called after validate returns without error.
   *
   * @param name - agent name
   * @param properties - embedded agent configuration
   * @return configuration applicable to a flume agent
   */
  @InterfaceAudience.Private
  @InterfaceStability.Unstable
  static Map<String, String> configure(String name,
      Map<String, String> properties) throws FlumeException {
    validate(name, properties);
    // we are going to modify the properties as we parse the config
    properties = new HashMap<String, String>(properties);

    if (!properties.containsKey(SOURCE_TYPE) ||
        SOURCE_TYPE_EMBEDDED_ALIAS.equalsIgnoreCase(properties.get(SOURCE_TYPE))) {
      properties.put(SOURCE_TYPE, SOURCE_TYPE_EMBEDDED);
    }
    String sinkNames = properties.remove(SINKS);

    String strippedName = name.replaceAll("\\s+","");
    String sourceName = "source-" + strippedName;
    String channelName = "channel-" + strippedName;
    String sinkGroupName = "sink-group-" + strippedName;

    /*
     * Now we are going to process the user supplied configuration
     * and generate an agent configuration. This is only to supply
     * a simpler client api than passing in an entire agent configuration.
     */
    // user supplied config -> agent configuration
    Map<String, String> result = Maps.newHashMap();

    /*
     * First we are going to setup all the root level pointers. I.E
     * point the agent at the components, sink group at sinks, and
     * source at the channel.
     */
    // point agent at source
    result.put(join(name, BasicConfigurationConstants.CONFIG_SOURCES),
        sourceName);
    // point agent at channel
    result.put(join(name, BasicConfigurationConstants.CONFIG_CHANNELS),
        channelName);
    // point agent at sinks
    result.put(join(name, BasicConfigurationConstants.CONFIG_SINKS),
        sinkNames);
    // points the agent at the sinkgroup
    result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS),
        sinkGroupName);
    // points the sinkgroup at the sinks
    result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS,
            sinkGroupName, SINKS), sinkNames);
    // points the source at the channel
    result.put(join(name,
        BasicConfigurationConstants.CONFIG_SOURCES, sourceName,
        BasicConfigurationConstants.CONFIG_CHANNELS), channelName);

    // Properties will be modified during iteration so we need a
    // copy of the keys.
    Set<String> userProvidedKeys = new HashSet<String>(properties.keySet());

    /*
     * Second process the sink configuration and point the sinks
     * at the channel.
     */
    for (String sink :  sinkNames.split("\\s+")) {
      for (String key : userProvidedKeys) {
        String value = properties.get(key);
        if (key.startsWith(sink + SEPERATOR)) {
          properties.remove(key);
          result.put(join(name,
              BasicConfigurationConstants.CONFIG_SINKS, key), value);
        }
      }
      // point the sink at the channel
      result.put(join(name,
          BasicConfigurationConstants.CONFIG_SINKS, sink,
          BasicConfigurationConstants.CONFIG_CHANNEL), channelName);
    }
    /*
     * Third, process all remaining configuration items, prefixing them
     * correctly and then passing them on to the agent.
     */
    userProvidedKeys = new HashSet<String>(properties.keySet());
    for (String key : userProvidedKeys) {
      String value = properties.get(key);
      if (key.startsWith(SOURCE_PREFIX)) {
        // users use `source' but agent needs the actual source name
        key = key.replaceFirst(SOURCE, sourceName);
        result.put(join(name,
            BasicConfigurationConstants.CONFIG_SOURCES, key), value);
      } else if (key.startsWith(CHANNEL_PREFIX)) {
        // users use `channel' but agent needs the actual channel name
        key = key.replaceFirst(CHANNEL, channelName);
        result.put(join(name,
            BasicConfigurationConstants.CONFIG_CHANNELS, key), value);
      } else if (key.startsWith(SINK_PROCESSOR_PREFIX)) {
        // agent.sinkgroups.sinkgroup.processor.*
        result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS,
                sinkGroupName, key), value);
      } else {
        // XXX should we simply ignore this?
        throw new FlumeException("Unknown configuration " + key);
      }
    }
    return result;
  }
  private static void checkAllowed(String[] allowedTypes, String type) {
    boolean isAllowed = false;
    type = type.trim();
    for (String allowedType : allowedTypes) {
      if (allowedType.equalsIgnoreCase(type)) {
        isAllowed = true;
        break;
      }
    }
    if (!isAllowed) {
      throw new FlumeException("Component type of " + type + " is not in " +
          "allowed types of " + Arrays.toString(allowedTypes));
    }
  }
  private static void checkRequired(Map<String, String> properties, String name) {
    if (!properties.containsKey(name)) {
      throw new FlumeException("Required parameter not found " + name);
    }
  }

  private static String join(String... parts) {
    return JOINER.join(parts);
  }

  private EmbeddedAgentConfiguration() {}
}