// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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.openqa.selenium.grid.config;

import com.beust.jcommander.Parameter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.Primitives;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.openqa.selenium.internal.Require;

/**
 * A form of {@link Config} that is generated by looking at fields in the constructor arg that are
 * annotated with {@link ConfigValue}. The class hierarchy is walked from closest to Object to the
 * constructor argument's type, null values are ignored, and the order in which fields are read is
 * not stable (meaning duplicate config values may give different values each time).
 *
 * <p>The main use of this class is to allow an object configured using (for example) jcommander to
 * be used directly within the app, without requiring intermediate support classes to transform
 * flags to config values.
 */
public class AnnotatedConfig implements Config {

  private final Map<String, Map<String, List<String>>> config;

  public AnnotatedConfig(Object obj) {
    this(obj, Collections.emptySet(), false);
  }

  public AnnotatedConfig(Object obj, Set<String> cliArgs, boolean includeCliArgs) {
    Map<String, Map<String, List<String>>> values = new HashMap<>();

    Deque<Field> allConfigValues = findConfigFields(obj.getClass());

    for (Field field : allConfigValues) {
      if (Map.class.isAssignableFrom(field.getType())) {
        throw new ConfigException("Map fields may not be used for configuration: " + field);
      }

      field.setAccessible(true);
      Object value;
      try {
        value = field.get(obj);
      } catch (IllegalAccessException e) {
        throw new ConfigException("Unable to read field: " + field);
      }

      ConfigValue annotation = field.getAnnotation(ConfigValue.class);
      Parameter cliAnnotation = field.getAnnotation(Parameter.class);
      boolean containsCliArg =
          cliAnnotation != null && Arrays.stream(cliAnnotation.names()).anyMatch(cliArgs::contains);
      if (!cliArgs.isEmpty() && !containsCliArg && includeCliArgs) {
        // Only getting config values for args entered by the user.
        continue;
      }
      if (!cliArgs.isEmpty() && containsCliArg && !includeCliArgs) {
        // Excluding config values for args entered by the user.
        continue;
      }
      Map<String, List<String>> section =
          values.computeIfAbsent(annotation.section(), str -> new HashMap<>());
      List<String> all = section.computeIfAbsent(annotation.name(), str -> new LinkedList<>());

      if (value instanceof Collection) {
        for (Object o : ((Collection<?>) value)) {
          String singleValue = getSingleValue(o);
          if (singleValue != null) {
            all.add(singleValue);
          }
        }
      } else {
        String singleValue = getSingleValue(value);
        if (singleValue != null) {
          all.add(singleValue);
        }
      }
    }

    // Now make the config immutable.
    this.config = values;
  }

  private String getSingleValue(Object value) {
    if (value == null) {
      return null;
    }

    if (value instanceof Map) {
      throw new ConfigException("Map fields may not be used for configuration: " + value);
    }

    if (value instanceof Collection) {
      throw new ConfigException("Collection fields may not be used for configuration: " + value);
    }

    if (Boolean.FALSE.equals(value) && !Primitives.isWrapperType(value.getClass())) {
      return null;
    }

    if (value instanceof Number && ((Number) value).floatValue() == 0f) {
      return null;
    }

    return String.valueOf(value);
  }

  private Deque<Field> findConfigFields(Class<?> clazz) {
    Deque<Field> toSet = new ArrayDeque<>();
    Set<Class<?>> toVisit = new HashSet<>();
    toVisit.add(clazz);

    Set<Class<?>> seen = new HashSet<>();

    while (!toVisit.isEmpty()) {
      clazz = toVisit.iterator().next();
      toVisit.remove(clazz);
      seen.add(clazz);

      Arrays.stream(clazz.getDeclaredFields())
          .filter(field -> field.getAnnotation(ConfigValue.class) != null)
          .forEach(toSet::addLast);

      Class<?> toAdd = clazz.getSuperclass();
      if (toAdd != null && !Object.class.equals(toAdd) && !seen.contains(toAdd)) {
        toVisit.add(toAdd);
      }
      Arrays.stream(clazz.getInterfaces())
          .filter(face -> !seen.contains(face))
          .forEach(toVisit::add);
    }

    return toSet;
  }

  @Override
  public Optional<List<String>> getAll(String section, String option) {
    Require.nonNull("Section name", section);
    Require.nonNull("Option name", option);

    Map<String, List<String>> sec = config.get(section);
    if (sec == null || sec.isEmpty()) {
      return Optional.empty();
    }

    List<String> values = sec.get(option);
    if (values == null || values.isEmpty()) {
      return Optional.empty();
    }

    return Optional.of(ImmutableList.copyOf(values));
  }

  @Override
  public Set<String> getSectionNames() {
    return ImmutableSortedSet.copyOf(config.keySet());
  }

  @Override
  public Set<String> getOptions(String section) {
    Require.nonNull("Section name to get options for", section);
    return ImmutableSortedSet.copyOf(config.getOrDefault(section, ImmutableMap.of()).keySet());
  }
}
