/** Storage for bean annotations info for Metadata Injection and Load/Save. */
public class BeanInjectionInfo {
  private static LogChannelInterface LOG =
      KettleLogStore.getLogChannelInterfaceFactory().create(BeanInjectionInfo.class);

  protected final Class<?> clazz;
  private final InjectionSupported clazzAnnotation;
  private Map<String, Property> properties = new HashMap<>();
  private List<Group> groupsList = new ArrayList<>();
  /** Used only for fast group search during initialize. */
  private Map<String, Group> groupsMap = new HashMap<>();

  public static boolean isInjectionSupported(Class<?> clazz) {
    InjectionSupported annotation = clazz.getAnnotation(InjectionSupported.class);
    return annotation != null;
  }

  public BeanInjectionInfo(Class<?> clazz) {
    LOG.logDebug("Collect bean injection info for " + clazz);
    try {
      this.clazz = clazz;
      clazzAnnotation = clazz.getAnnotation(InjectionSupported.class);
      if (clazzAnnotation == null) {
        throw new RuntimeException("Injection not supported in " + clazz);
      }

      Group gr0 = new Group("");
      groupsList.add(gr0);
      groupsMap.put(gr0.getName(), gr0);
      for (String group : clazzAnnotation.groups()) {
        Group gr = new Group(group);
        groupsList.add(gr);
        groupsMap.put(gr.getName(), gr);
      }

      BeanLevelInfo root = new BeanLevelInfo();
      root.leafClass = clazz;
      root.init(this);

      properties = Collections.unmodifiableMap(properties);
      groupsList = Collections.unmodifiableList(groupsList);
      groupsMap = null;
    } catch (Throwable ex) {
      LOG.logError(
          "Error bean injection info collection for " + clazz + ": " + ex.getMessage(), ex);
      throw ex;
    }
  }

  public String getLocalizationPrefix() {
    return clazzAnnotation.localizationPrefix();
  }

  public Map<String, Property> getProperties() {
    return properties;
  }

  public List<Group> getGroups() {
    return groupsList;
  }

  protected void addInjectionProperty(Injection metaInj, BeanLevelInfo leaf) {
    if (StringUtils.isBlank(metaInj.name())) {
      throw new RuntimeException("Property name shouldn't be blank in the " + clazz);
    }

    String propertyName = calcPropertyName(metaInj, leaf);
    if (properties.containsKey(propertyName)) {
      throw new RuntimeException("Property '" + propertyName + "' already defined for " + clazz);
    }
    Property prop = new Property(propertyName, metaInj.group(), leaf.createCallStack());
    properties.put(prop.name, prop);
    Group gr = groupsMap.get(metaInj.group());
    if (gr == null) {
      throw new RuntimeException(
          "Group '"
              + metaInj.group()
              + "' for property '"
              + metaInj.name()
              + "' is not defined "
              + clazz);
    }
    gr.groupProperties.add(prop);
  }

  public String getDescription(String name) {
    String description = BaseMessages.getString(clazz, clazzAnnotation.localizationPrefix() + name);
    if (description != null && description.startsWith("!") && description.endsWith("!")) {
      Class baseClass = clazz.getSuperclass();
      while (baseClass != null) {
        InjectionSupported baseAnnotation =
            (InjectionSupported) baseClass.getAnnotation(InjectionSupported.class);
        if (baseAnnotation != null) {
          description =
              BaseMessages.getString(baseClass, baseAnnotation.localizationPrefix() + name);
          if (description != null && !description.startsWith("!") && !description.endsWith("!")) {
            return description;
          }
        }
        baseClass = baseClass.getSuperclass();
      }
    }
    return description;
  }

  private String calcPropertyName(Injection metaInj, BeanLevelInfo leaf) {
    String name = metaInj.name();
    while (leaf != null) {
      if (StringUtils.isNotBlank(leaf.prefix)) {
        name = leaf.prefix + '.' + name;
      }
      leaf = leaf.parent;
    }
    if (!name.equals(metaInj.name()) && !metaInj.group().isEmpty()) {
      // group exist with prefix
      throw new RuntimeException("Group shouldn't be declared with prefix in " + clazz);
    }
    return name;
  }

  public class Property {
    private final String name;
    private final String groupName;
    protected final List<BeanLevelInfo> path;
    protected final int pathArraysCount;

    public Property(String name, String groupName, List<BeanLevelInfo> path) {
      this.name = name;
      this.groupName = groupName;
      this.path = path;
      int ac = 0;
      for (BeanLevelInfo level : path) {
        if (level.dim != BeanLevelInfo.DIMENSION.NONE) {
          ac++;
        }
      }
      pathArraysCount = ac;
    }

    public String getName() {
      return name;
    }

    public String getGroupName() {
      return groupName;
    }

    public String getDescription() {
      return BeanInjectionInfo.this.getDescription(name);
    }

    public Class<?> getPropertyClass() {
      return path.get(path.size() - 1).leafClass;
    }
  }

  public class Group {
    private final String name;
    protected final List<Property> groupProperties = new ArrayList<>();

    public Group(String name) {
      this.name = name;
    }

    public String getName() {
      return name;
    }

    public List<Property> getGroupProperties() {
      return Collections.unmodifiableList(groupProperties);
    }

    public String getDescription() {
      return BeanInjectionInfo.this.getDescription(name);
    }
  }
}