{-# language
    AllowAmbiguousTypes
  , DeriveGeneric
  , DeriveLift
  , DerivingStrategies
#-}

module Shwifty.Types
  ( Ty(..)
  , SwiftData(..)
  , Protocol(..)
  , Options(..)
  ) where

import GHC.Generics (Generic)
import Language.Haskell.TH.Syntax (Lift)

-- | An AST representing a Swift type.
data Ty
  = Unit
    -- ^ Unit (called "Unit/Void" in swift). Empty struct type.
  | Bool
    -- ^ Bool
  | Character
    -- ^ Character
  | Str
    -- ^ String. Named 'Str' to avoid conflicts with
    --   'Data.Aeson.String'.
  | I
    -- ^ signed machine integer
  | I8
    -- ^ signed 8-bit integer
  | I16
    -- ^ signed 16-bit integer
  | I32
    -- ^ signed 32-bit integer
  | I64
    -- ^ signed 64-bit integer
  | U
    -- ^ unsigned machine integer
  | U8
    -- ^ unsigned 8-bit integer
  | U16
    -- ^ unsigned 16-bit integer
  | U32
    -- ^ unsigned 32-bit integer
  | U64
    -- ^ unsigned 64-bit integer
  | F32
    -- ^ 32-bit floating point
  | F64
    -- ^ 64-bit floating point
  | Decimal
    -- ^ Increased-precision floating point
  | BigSInt32
    -- ^ 32-bit big integer
  | BigSInt64
    -- ^ 64-bit big integer
  | Tuple2 Ty Ty
    -- ^ 2-tuple
  | Tuple3 Ty Ty Ty
    -- ^ 3-tuple
  | Optional Ty
    -- ^ Maybe type
  | Result Ty Ty
    -- ^ Either type
    --
    --   /Note/: The error type in Swift must
    --   implement the @Error@ protocol. This library
    --   currently does not enforce this.
  | Set Ty
    -- ^ Set type
  | Dictionary Ty Ty
    -- ^ Dictionary type
  | Array Ty
    -- ^ array type
  | App Ty Ty
    -- ^ function type
  | Poly String
    -- ^ polymorphic type variable
  | Concrete
      { concreteName :: String
        -- ^ the name of the type
      , concreteTyVars :: [Ty]
        -- ^ the type's type variables
      }
    -- ^ a concrete type variable, and its
    --   type variables. Will typically be generated
    --   by 'getShwifty'.
  | Tag
      { tagName :: String
        -- ^ the name of the type
      , tagParent :: String
        -- ^ the type constructor of the type
        --   to which this alias belongs
      , tagTyp :: Ty
        -- ^ the type that this represents
      , tagDisambiguate :: Bool
        -- ^ does the type need disambiguation?
        --
        --   This will happen if there are multiple
        --   tags with the same type. This is needed
        --   to maintain safety.
      }
    -- ^ A @Tagged@ typealias, for newtyping
    --   in a way that doesn't break Codable.
    --
    --   See 'getShwiftyWithTags' for examples.
  deriving stock (Eq, Show, Read)
  deriving stock (Generic)
  deriving stock (Lift)

-- | A Swift datatype, either a struct (product type)
--   or enum (sum type). Haskll types are
--   sums-of-products, so the way we differentiate
--   when doing codegen,
--   is that types with a single constructor
--   will be converted to a struct, and those with
--   two or more constructors will be converted to an
--   enum. Types with 0 constructors will be converted
--   to an empty enum.
data SwiftData
  = SwiftStruct
      { structName :: String
        -- ^ The name of the struct
      , structTyVars :: [String]
        -- ^ The struct's type variables
      , structProtocols :: [Protocol]
        -- ^ The protocols which the struct
        --   implements
      , structFields :: [(String, Ty)]
        -- ^ The fields of the struct. the pair
        --   is interpreted as (name, type).
      , structPrivateTypes :: [SwiftData]
        -- ^ Private types of the struct. Typically
        --   populated by setting 'makeBase'.
      , structTags :: [Ty]
        -- ^ The tags of the struct. See 'Tag'.
      }
    -- ^ A struct (product type)
  | SwiftEnum
      { enumName :: String
        -- ^ The name of the enum
      , enumTyVars :: [String]
        -- ^ The enum's type variables
      , enumProtocols :: [Protocol]
        -- ^ The protocols which the enum
        --   implements
      , enumCases :: [(String, [(Maybe String, Ty)])]
        -- ^ The cases of the enum. the type
        --   can be interpreted as
        --   (name, [(label, type)]).
      , enumRawValue :: Maybe Ty
        -- ^ The rawValue of an enum. See
        --   https://developer.apple.com/documentation/swift/rawrepresentable/1540698-rawvalue
        --
        --   Typically the 'Ty' will be
        --   'I' or 'String'.
        --
        --   /Note/: Currently, nothing will prevent
        --   you from putting something
        --   nonsensical here.
      , enumPrivateTypes :: [SwiftData]
        -- ^ Private types of the enum. Typically
        --   populated by setting 'makeBase'.
      , enumTags :: [Ty]
        -- ^ The tags of the struct. See 'Tag'.
      }
    -- ^ An enum (sum type)
  | SwiftAlias
      { aliasName :: String
        -- ^ the name of the type alias
      , aliasTyVars :: [String]
        -- ^ the type variables of the type alias
      , aliasTyp :: Ty
        -- ^ the type this represents (RHS)
      }
    -- ^ A /top-level/ type alias
  deriving stock (Eq, Read, Show, Generic)

-- | Swift protocols.
--   Only a few are supported right now.
data Protocol
  = Hashable
    -- ^ The 'Hashable' protocol.
    --   See https://developer.apple.com/documentation/swift/hashable
  | Codable
    -- ^ The 'Codable' protocol.
    --   See https://developer.apple.com/documentation/swift/hashable
  | Equatable
    -- ^ The 'Equatable' protocol.
    --   See https://developer.apple.com/documentation/swift/hashable
  deriving stock (Eq, Read, Show, Generic)
  deriving stock (Lift)

-- | Options that specify how to
--   encode your 'SwiftData' to a swift type.
--
--   Options can be set using record syntax on
--   'defaultOptions' with the fields below.
data Options = Options
  { typeConstructorModifier :: String -> String
    -- ^ Function applied to type constructor names.
    --   The default ('id') makes no changes.
  , fieldLabelModifier :: String -> String
    -- ^ Function applied to field labels.
    --   Handy for removing common record prefixes,
    --   for example. The default ('id') makes no
    --   changes.
  , constructorModifier :: String -> String
    -- ^ Function applied to value constructor names.
    --   The default ('id') makes no changes.
  , optionalExpand :: Bool
    -- ^ Whether or not to truncate Optional types.
    --   Normally, an Optional ('Maybe') is encoded
    --   as "A?", which is syntactic sugar for
    --   "Optional\<A\>". The default value ('False')
    --   will keep it as sugar. A value of 'True'
    --   will expand it to be desugared.
  , generateToSwift :: Bool
    -- ^ Whether or not to generate a 'ToSwift'
    --   instance. Sometime this can be desirable
    --   if you want to define the instance by hand,
    --   or the instance exists elsewhere.
    --   The default is 'True', i.e., to generate
    --   the instance.
  , generateToSwiftData :: Bool
    -- ^ Whether or not to generate a 'ToSwiftData'
    --   instance. Sometime this can be desirable
    --   if you want to define the instance by hand,
    --   or the instance exists elsewhere.
    --   The default is 'True', i.e., to generate
    --   the instance.
  , dataProtocols :: [Protocol]
    -- ^ Protocols to add to a type.
    --   The default (@[]@) will add none.
  , dataRawValue :: Maybe Ty
    -- ^ The rawValue of an enum. See
    --   https://developer.apple.com/documentation/swift/rawrepresentable/1540698-rawvalue
    --
    --   The default ('Nothing') will not
    --   include any rawValue.
    --
    --   Typically, if the type does have
    --   a 'rawValue', the 'Ty' will be
    --   'I' or 'Str'.
    --
    --   /Note/: Currently, nothing will prevent
    --   you from putting something
    --   nonsensical here.
  , typeAlias :: Bool
    -- ^ Whether or not to generate a newtype as
    --   a type alias. Consider if you want this
    --   or to use 'getShwiftyWithTags' instead.
    --
    --   The default ('False') will generate newtypes
    --   as their own structs.
  , newtypeTag :: Bool
    -- ^ Whether or not to generate a newtype as an
    --   empty enum with a tag. This is for type
    --   safety reasons, but with retaining the
    --   ability to have Codable conformance.
    --
    --   The default ('False') will not do this.
    --
    --   /Note/: This takes priority over 'typeAlias'.
    --
    --   /Note/: This option is not currently
    --   supported for newtype instances.
    --
    -- === __Examples__
    --
    -- > newtype NonEmptyText = MkNonEmptyText String
    -- > $(getShwiftyWith (defaultOptions { newtypeTag = True }) ''NonEmpyText)
    --
    -- @
    -- enum NonEmptyTextTag {
    --     typealias NonEmptyText = Tagged\<NonEmptyTextTag, String\>
    -- }
    -- @
  , lowerFirstField :: Bool
    -- ^ Whether or not to lower-case the first
    --   character of a field after applying all
    --   modifiers to it.
    --
    --   The default ('True') will do so.
  , lowerFirstCase :: Bool
    -- ^ Whether or not to lower-case the first
    --   character of a case after applying all
    --   modifiers to it.
    --
    --   The default ('True') will do so.
  , omitFields :: [String]
    -- ^ Fields to omit from a struct when
    --   generating types.
    --
    --   The default (@[]@) will omit nothing.
  , omitCases :: [String]
    -- ^ Cases to omit from an enum when
    --   generating types.
    --
    --   The default (@[]@) will omit nothing.
  , makeBase :: (Bool, Maybe Ty, [Protocol])
    -- ^ Whether or not to make a base type,
    --   its raw value, and its protocols.
    --
    --   Here, "base type" refers to a
    --   version of the type without any fields.
    --   This can be useful for doing Codable
    --   conversions.
    --
    --   The default ('False', 'Nothing', @[]@)
    --   will not create the base type.
  }