-- 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