content: -- Copyright (c) Microsoft. All rights reserved.
  -- Licensed under the MIT license. See LICENSE file in the project root
  -- for full license information.
  
  {-# LANGUAGE QuasiQuotes, OverloadedStrings, RecordWildCards #-}
  
  module Language.Bond.Codegen.Java.Enum_java
      ( enum_java
      ) where
  
  import Prelude
  import Data.Text.Lazy (Text)
  import Text.Shakespeare.Text
  import Language.Bond.Syntax.Types
  import Language.Bond.Util
  import Language.Bond.Codegen.TypeMapping
  import Language.Bond.Codegen.Util
  import qualified Language.Bond.Codegen.Java.Util as Java
  
  -- Template for enum -> Java enum-like class.
  enum_java :: MappingContext -> Declaration -> Text
  enum_java java declaration = [lt|
  package #{javaPackage};
  
  #{typeDefinition declaration}
  |]
    where
      javaPackage = sepBy "." toText $ getNamespace java
  
      typeDefinition Enum {..} = [lt|
  #{Java.generatedClassAnnotations}
  public final class #{declName} implements org.bondlib.BondEnum<#{declName}> {
  
      public static final class Values {
          private Values() {}
  
          #{newlineSep 2 constantIntValueDecl enumConstantsWithInt}
      }
  
      private static final class EnumBondTypeImpl extends org.bondlib.EnumBondType<#{declName}> {
  
          @Override
          public java.lang.Class<#{declName}> getValueClass() { return #{declName}.class; }
  
          @Override
          public final #{declName} getEnumValue(int value) { return get(value); }
      }
  
      public static final org.bondlib.EnumBondType<#{declName}> BOND_TYPE = new EnumBondTypeImpl();
  
      #{newlineSep 1 constantObjectDecl enumConstantsWithInt}
  
      public final int value;
  
      private final java.lang.String label;
  
      private #{declName}(int value, java.lang.String label) { this.value = value; this.label = label; }
  
      @Override
      public final int getValue() { return this.value; }
  
      @Override
      public final java.lang.String getLabel() { return this.label; }
  
      @Override
      public final org.bondlib.EnumBondType<#{declName}> getBondType() { return BOND_TYPE; }
  
      @Override
      public final int compareTo(#{declName} o) { return this.value < o.value ? -1 : (this.value > o.value ? 1 : 0); }
  
      @Override
      public final boolean equals(java.lang.Object other) { return (other instanceof #{declName}) && (this.value == ((#{declName}) other).value); }
  
      @Override
      public final int hashCode() { return this.value; }
  
      @Override
      public final java.lang.String toString() { return this.label != null ? this.label : ("#{declName}(" + java.lang.String.valueOf(this.value) + ")"); }
  
      public static #{declName} get(int value) {
          switch (value) {
              #{newlineSep 3 switchCaseConstantMapping enumConstantsWithIntDistinct}
              default: return new #{declName}(value, null);
          }
      }
  
      public static #{declName} valueOf(java.lang.String str) {
          if (str == null) {
              throw new java.lang.IllegalArgumentException("Argument 'str' must not be null.");
          #{newlineSepEnd 2 parseCaseConstantMapping enumConstants}} else {
              throw new java.lang.IllegalArgumentException("Invalid '#{declName}' enum value: '" + str + "'.");
          }
      }
  }|]
        where
          -- constant object
          constantObjectDecl c@Constant {..} =
              [lt|public static final #{declName} #{constantName} = #{enumObjectAssigmentValue c};|]
  
          -- constant int
          constantIntValueDecl Constant {..} = let value x = [lt|#{x}|] in
              [lt|public static final int #{constantName} = #{optional value constantValue};|]
  
          -- switch cases that map int to object
          switchCaseConstantMapping Constant {..} =
              [lt|case Values.#{constantName}: return #{constantName};|]
  
          -- parse cases that map string to object
          parseCaseConstantMapping Constant {..} =
              [lt|} else if (str.equals("#{constantName}")) {
              return #{constantName};|]
  
          -- Process constants to make sure every constant value is set (either explicit or auto-generated).
          -- TODO: auto-generation of constant values should be handled earlier, once for all languages.
          enumConstantsWithInt = fixEnumWithInt 0 enumConstants []
  
          fixEnumWithInt :: Int -> [Constant] -> [Constant] -> [Constant]
          fixEnumWithInt _ [] result = reverse result
          fixEnumWithInt nextInt (h:r) result = case constantValue h of
            Just n -> let fixedN = (fromInteger (Java.twosComplement 32 (toInteger n))) in
              fixEnumWithInt (fixedN + 1) r ((Constant (constantName h) (Just fixedN)):result)
            Nothing -> fixEnumWithInt (nextInt + 1) r ((Constant (constantName h) (Just nextInt)):result)
  
          -- Filter a list of constants, leaving a list of constants with distinct values.
          -- If several constants in the input list share a value, the first one that appears will be the one that appears in the output list.
          enumConstantsWithIntDistinct = findEnumConstantsDistinct enumConstantsWithInt []
  
          findEnumConstantsDistinct :: [Constant] -> [Maybe Int] -> [Constant]
          findEnumConstantsDistinct [] _ = []
          findEnumConstantsDistinct (h:r) keys = if elem (constantValue h) keys
              then findEnumConstantsDistinct r keys
              else h : findEnumConstantsDistinct r ((constantValue h):keys)
  
          -- The RHS value to assign to an enum object. If an enum element is the first instance with a particular value,
          -- this function will return an instantiation. If it isn't, this function will return a reference to the first enum object with the same value.
          enumObjectAssigmentValue :: Constant -> Text
          enumObjectAssigmentValue enumConstant =
              let firstDeclaredEnumConstant = head (filter (\c -> (constantValue c) == (constantValue enumConstant)) enumConstantsWithIntDistinct) in
                  if firstDeclaredEnumConstant == enumConstant
                      then [lt|new #{declName}(Values.#{constantName enumConstant}, "#{constantName enumConstant}")|]
                      else [lt|#{constantName firstDeclaredEnumConstant}|]
  
      typeDefinition _ = mempty
  