{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module SchemaGeneration
    ( Schema (..), genSchema
    ) where

import Control.Monad.State.Strict

import Data.List.NonEmpty (NonEmpty((:|)))

import qualified Data.List.NonEmpty as NE
import qualified Test.QuickCheck    as QC

-- Definitions

newtype FieldName
  = FieldName String

instance Show FieldName where
  show (FieldName fn) = fn

newtype StructName
  = StructName String

instance Show StructName where
  show (StructName fn) = fn

data Field
    = FieldDef FieldName Int FieldType
    | StructDef StructName [Field]

data BuiltIn
    = Void
    | Bool
    | Int8
    | Int16
    | Int32
    | Int64
    | UInt8
    | UInt16
    | UInt32
    | UInt64
    | Float32
    | Float64
    | Text
    | Data
    deriving (Show, Enum)

data FieldType
    = BasicType BuiltIn
    | ListType FieldType
    | StructType StructName

instance Show FieldType where
    show (BasicType bi)  = show bi
    show (ListType ft)   = "List(" ++ show ft ++ ")"
    show (StructType sn) = show sn

instance Show Field where
    show (FieldDef name order entryType) = concat
       [ show name, " @", show order, " :"
       , show entryType, ";\n"
       ]
    show (StructDef name content) = concat
       [ "struct ", show name, " {\n"
       , concatMap (('\t':) . show) content
       , "}\n\n"
       ]

data Schema = Schema
      { schemaId      :: String
      , schemaContent :: [Field]
      }

instance Show Schema where
    show s = concat
        [ "@0x", schemaId s, ";\n\n"
        , concatMap show (schemaContent s)
        ]

-- Helper generators

genSafeLCChar :: QC.Gen Char
genSafeLCChar = QC.elements ['a'..'z']

genSafeUCChar :: QC.Gen Char
genSafeUCChar = QC.elements ['A'..'Z']

genSafeHexChar :: QC.Gen Char
genSafeHexChar = QC.elements (['0'..'9'] ++ ['a'..'f'])

newtype FieldGen a
    = FieldGen (StateT (NonEmpty (Int, Int)) QC.Gen a)
    deriving (Functor, Applicative, Monad)

liftGen :: QC.Gen a -> FieldGen a
liftGen m = FieldGen (lift m)

runFieldGen :: FieldGen a -> QC.Gen a
runFieldGen (FieldGen m) = fst <$> runStateT m ((0, 0) :| [])

pushFieldGen :: FieldGen ()
pushFieldGen = FieldGen $ modify (NE.cons (0, 0))

popFieldGen :: FieldGen ()
popFieldGen = FieldGen $ do
    original <- get
    case original of
        (_ :| (y : rest)) -> put (y :| rest)
        (x :| [])         -> put (x :| [])

getStructOrder :: FieldGen Int
getStructOrder = FieldGen $ do
    current <- get
    let (result, _) = NE.head current
    case current of
        ((x, y) :| rest) -> put ((x + 1, y) :| rest)
    return result

getOrder :: FieldGen Int
getOrder = FieldGen $ do
    current <- get
    let (_, result) = NE.head current
    case current of
        ((x, y) :| rest) -> put ((x + 1, y + 1) :| rest)
    return result

-- Field types

-- need to enumerate each field; this will be performed during struct
-- generation where the number of fields is known (numberDefs)
genFieldDef :: [FieldType] -> FieldGen Field
genFieldDef structTypes = do
    order <- getOrder
    fieldName <- do
        str <- liftGen $ QC.listOf1 genSafeLCChar
        return $ FieldName (str ++ show order)
    fieldType <- liftGen $ QC.elements (map BasicType [Bool ..] ++ structTypes)
    return $ FieldDef fieldName order fieldType

-- Struct type

-- like fields, we enumerate each struct during generation for uniqueness
genStructDef :: Int -> FieldGen Field
genStructDef depth = do
    order <- getStructOrder

    pushFieldGen

    -- generate the struct's name
    structName <- do
        fc <- liftGen genSafeUCChar
        rest <- liftGen (QC.listOf genSafeLCChar)
        return $ StructName ((fc:rest) ++ show order)

    -- generate the nested structs
    structNum  <- if depth <= 0
                    then pure 0
                    else liftGen (QC.choose (0, 3))
    structDefs <- replicateM structNum (genStructDef (depth - 1))

    -- extract the available struct types
    let structTypes = map (\(StructDef sn _) -> StructType sn) structDefs

    -- generate the fields using available struct types
    fieldNum  <- liftGen (QC.sized (\n -> QC.choose (1, 1 `max` n)))
    fieldDefs <- replicateM fieldNum (genFieldDef structTypes)

    popFieldGen

    return $ StructDef structName (fieldDefs ++ structDefs)


-- Schema type
genSchema :: QC.Gen Schema
genSchema = do
    id1st <- QC.elements ['a'..'f']
    idrest <- QC.vectorOf 15 genSafeHexChar
    -- multiple structs make tests take too long
    content <- runFieldGen (genStructDef 3)
    return $ Schema (id1st:idrest) [content]
