package org.infinispan.protostream.schema;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.infinispan.protostream.impl.Log;
import org.infinispan.protostream.impl.SparseBitSet;

/**
 * @since 5.0
 */
public class Enum {
   private final String name;
   private final String fullName;
   private final Map<String, Object> options;
   private final List<String> comments;
   private final Map<String, EnumValue> values;
   private final ReservedNumbers reservedNumbers;
   private final Set<String> reservedNames;

   private Enum(Builder builder) {
      this.name = builder.name;
      this.fullName = builder.getFullName();
      AtomicInteger autoNumber = new AtomicInteger(0);
      this.values = builder.values.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> e.getValue().create(autoNumber)));
      this.reservedNumbers = builder.reservedNumbers;
      this.reservedNames = Set.copyOf(builder.reservedNames);
      this.options = Map.copyOf(builder.options);
      this.comments = List.copyOf(builder.comments);
   }

   public String getName() {
      return name;
   }

   public String getFullName() {
      return fullName;
   }

   public Map<String, EnumValue> getValues() {
      return values;
   }

   public Map<String, Object> getOptions() {
      return options;
   }

   public List<String> getComments() {
      return comments;
   }

   public ReservedNumbers getReservedNumbers() {
      return reservedNumbers;
   }

   public Set<String> getReservedNames() {
      return reservedNames;
   }

   public static class Builder implements CommentContainer<Builder>, ReservedContainer<Builder>, OptionContainer<Builder> {
      private final EnumContainer parent;
      private final String name;
      private final Map<String, EnumValue.Builder> values = new LinkedHashMap<>();
      private final SparseBitSet reservedNumbers = new SparseBitSet();
      private final Set<String> reservedNames = new HashSet<>();
      private final Map<String, Object> options = new HashMap<>();
      private final List<String> comments = new ArrayList<>();

      Builder(EnumContainer parent, String name) {
         this.parent = parent;
         this.name = name;
      }

      public EnumValue.Builder addValue(String name) {
         return addValue(name, -1);
      }

      public EnumValue.Builder addValue(String name, int number) {
         Objects.requireNonNull(name, "name must not be null");
         checkDuplicate(name);
         EnumValue.Builder value = new EnumValue.Builder(this, name, number);
         values.put(name, value);
         return value;
      }

      private void checkDuplicate(String name) {
         if (values.containsKey(name) || reservedNames.contains(name)) {
            throw new IllegalArgumentException("Duplicate name " + name);
         }
      }

      @Override
      public Builder addReserved(int... numbers) {
         for (int number : numbers) {
            reservedNumbers.set(number);
         }
         return this;
      }

      @Override
      public Builder addReservedRange(int from, int to) {
         reservedNumbers.set(from, to + 1);
         return this;
      }

      @Override
      public Builder addReserved(String name) {
         Objects.requireNonNull(name, "name must not be null");
         reservedNames.add(name);
         return this;
      }

      @Override
      public Builder addOption(String name, Object value) {
         Objects.requireNonNull(name, "name must not be null");
         Objects.requireNonNull(value, "value must not be null");
         options.put(name, value);
         return this;
      }

      @Override
      public Builder addComment(String comment) {
         Objects.requireNonNull(comment, "comment must not be null");
         comments.add(comment.trim());
         return this;
      }

      public Message.Builder addMessage(String name) {
         return parent.addMessage(name);
      }

      public Enum.Builder addEnum(String name) {
         return parent.addEnum(name);
      }

      private void validate() {
         Boolean autoGenerateNumbers = null;
         boolean hasZero = false;
         for (EnumValue.Builder v : values.values()) {
            if (reservedNames.contains(v.name)) {
               throw Log.LOG.reservedName(v.name, name);
            }
            if (v.number < 0) {
               if (Boolean.FALSE.equals(autoGenerateNumbers)) {
                  throw Log.LOG.cannotMixAutoGeneratedNumbers(name);
               } else {
                  autoGenerateNumbers = Boolean.TRUE;
               }
            } else {
               if (reservedNumbers.get(v.number)) {
                  throw Log.LOG.reservedNumber(v.number, v.name, name);
               }
               if (Boolean.TRUE.equals(autoGenerateNumbers)) {
                  throw Log.LOG.cannotMixAutoGeneratedNumbers(name);
               } else {
                  autoGenerateNumbers = Boolean.FALSE;
               }
               if (v.number == 0) {
                  hasZero = true;
               }
            }
         }
         if (!hasZero && Boolean.FALSE.equals(autoGenerateNumbers)) {
            throw Log.LOG.missingDefaultEnumValue(name);
         }
      }

      Enum create() {
         validate();
         return new Enum(this);
      }

      public Schema build() {
         return parent.build();
      }

      @Override
      public String getFullName() {
         return parent.getFullName() + '.' + name;
      }
   }
}
