{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-|
Top-Level data types for B9 build artifacts.
-}
module B9.ArtifactGenerator
  (ArtifactGenerator(..)
  ,ArtifactSource(..)
  ,InstanceId(..)
  ,ArtifactTarget(..)
  ,CloudInitType(..)
  ,ArtifactAssembly(..)
  ,AssembledArtifact(..)
  --,YamlValue (..)
  ,instanceIdKey
  ,buildIdKey
  ,buildDateKey
  ) where


import Data.Data
import Data.Monoid -- hiding ((<>))
import Control.Applicative

import B9.DiskImages
import B9.Vm
import B9.Content.StringTemplate
import B9.Content.Generator
import B9.QCUtil

import Test.QuickCheck

{-| Artifacts represent the things B9 can build. A generator specifies howto
generate parameterized, multiple artifacts. The general structure is:

@
   Let [ ... bindings ... ]
       [ Sources
           [ ... list all input files ... ]
           [ Artifact ...
           , Artifact ...
           , Let [ ... ] [ ... ]
           ]
       ]
@

The reasons why 'Sources' takes a list of 'ArtifactGenerator's is that
1. this makes the value easier to read/write for humans
2. the sources are static files used in all children (e.g. company logo image)
3. the sources are parameterized by variables that bound to different values
   for each artifact, e.g. a template network config file which contains
   the host IP address.

To bind such variables use 'Let', 'Each', 'LetX' or 'EachT'.

String subtitution of these variables is done by "B9.Content.StringTemplate".
These variables can be used as value in nested 'Let's, in most file names/paths
and in source files added with 'B9.Content.StringTemplate.SourceFile'

-}
data ArtifactGenerator = Sources [ArtifactSource] [ArtifactGenerator]
                       -- ^ Add sources available to 'ArtifactAssembly's in
                       -- nested artifact generators.
                       | Let [(String, String)] [ArtifactGenerator]
                       -- ^ Bind variables, variables are avaible in nested
                       -- generators.
                       | LetX [(String, [String])] [ArtifactGenerator]
                       -- ^ A 'Let' where each variable is assigned to each
                       -- value; the nested generator is executed for each
                       -- permutation.
                       --
                       -- @
                       --     LetX [("x", ["1","2","3"]), ("y", ["a","b"])] [..]
                       -- @
                       -- Is equal to:
                       --
                       -- @
                       --     Let [] [
                       --       Let [("x", "1"), ("y", "a")] [..]
                       --       Let [("x", "1"), ("y", "b")] [..]
                       --       Let [("x", "2"), ("y", "a")] [..]
                       --       Let [("x", "2"), ("y", "b")] [..]
                       --       Let [("x", "3"), ("y", "a")] [..]
                       --       Let [("x", "3"), ("y", "b")] [..]
                       --     ]
                       -- @
                       | Each [(String,[String])] [ArtifactGenerator]
                       -- ^ Bind each variable to their first value, then each
                       -- variable to the second value, etc ... and execute the
                       -- nested generator in every step. 'LetX' represents a
                       -- product of all variables, whereas 'Each' represents a
                       -- sum of variable bindings - 'Each' is more like a /zip/
                       -- whereas 'LetX' is more like a list comprehension.
                       | EachT [String] [[String]] [ArtifactGenerator]
                       -- ^ The transposed verison of 'Each': Bind the variables
                       -- in the first list to each a set of values from the
                       -- second argument; execute the nested generators for
                       -- each binding
                       | Artifact InstanceId ArtifactAssembly
                       -- ^ Generate an artifact defined by an
                       -- 'ArtifactAssembly'; the assembly can access the files
                       -- created from the 'Sources' and variables bound by
                       -- 'Let'ish elements. An artifact has an instance id,
                       -- that is a unique, human readable string describing the
                       -- artifact to assemble.
                       | EmptyArtifact
                       deriving (Read, Show, Eq)

instance Monoid ArtifactGenerator where
  mempty = Let [] []
  (Let [] []) `mappend` x = x
  x `mappend` (Let [] []) = x
  x `mappend` y = Let [] [x, y]

-- | Describe how input files for artifacts to build are obtained.  The general
--   structure of each constructor is __FromXXX__ /destination/ /source/
data ArtifactSource = FromFile FilePath SourceFile
                     -- ^ Copy a 'B9.Content.StringTemplate.SourceFile'
                     -- potentially replacing variabled defined in 'Let'-like
                     -- parent elements.
                    | FromContent FilePath Content
                     -- ^ Create a file from some 'Content'
                    | SetPermissions Int Int Int [ArtifactSource]
                     -- ^ Set the unix /file permissions/ to all files generated
                     -- by the nested list of 'ArtifactSource's.
                    | FromDirectory FilePath [ArtifactSource]
                     -- ^ Assume a local directory as starting point for all
                     -- relative source files in the nested 'ArtifactSource's.
                    | IntoDirectory FilePath [ArtifactSource]
                     -- ^ Specify an output directory for all the files
                     -- generated by the nested 'ArtifactSource's
                    | Concatenation FilePath [ArtifactSource]
                     -- ^ __Deprecated__ Concatenate the files generated by the
                     -- nested 'ArtifactSource's. The nested, generated files
                     -- are not written when they are concatenated.
    deriving (Read, Show, Eq)

-- | Identify an artifact. __Deprecated__ TODO: B9 does not check if all
-- instances IDs are unique.
newtype InstanceId = IID String
  deriving (Read, Show, Typeable, Data, Eq)

-- | The variable containing the instance id. __Deprecated__
instanceIdKey :: String
instanceIdKey = "instance_id"

-- | The variable containing the buildId that identifies each execution of
-- B9. For more info about variable substitution in source files see
-- 'B9.Content.StringTemplate'
buildIdKey :: String
buildIdKey = "build_id"

-- | The variable containing the date and time a build was started. For more
-- info about variable substitution in source files see
-- 'B9.Content.StringTemplate'
buildDateKey :: String
buildDateKey = "build_date"

-- | Define an __output__ of a build. Assemblies are nested into
-- 'ArtifactGenerator's. They contain all the files defined by the 'Sources'
-- they are nested into.
data ArtifactAssembly = CloudInit [CloudInitType] FilePath
                       -- ^ Generate a __cloud-init__ compatible directory, ISO-
                       -- or VFAT image, as specified by the list of
                       -- 'CloudInitType's. Every item will use the second
                       -- argument to create an appropriate /file name/,
                       -- e.g. for the 'CI_ISO' type the output is @second_param.iso@.
                      | VmImages [ImageTarget] VmScript
                       -- ^ a set of VM-images that were created by executing a
                       -- build script on them.
  deriving (Read, Show, Typeable, Data, Eq)

-- | A type representing the targets assembled by
-- 'B9.ArtifactGeneratorImpl.assemble' from an 'ArtifactAssembly'. There is a
-- list of 'ArtifactTarget's because e.g. a single 'CloudInit' can produce upto
-- three output files, a directory, an ISO image and a VFAT image.
data AssembledArtifact = AssembledArtifact InstanceId [ArtifactTarget]
  deriving (Read, Show, Typeable, Data, Eq)

data ArtifactTarget = CloudInitTarget CloudInitType FilePath
                    | VmImagesTarget
  deriving (Read, Show, Typeable, Data, Eq)

data CloudInitType = CI_ISO | CI_VFAT | CI_DIR
  deriving (Read, Show, Typeable, Data, Eq)

instance Arbitrary ArtifactGenerator where
  arbitrary = oneof [ Sources <$> (halfSize arbitrary) <*> (halfSize arbitrary)
                    , Let <$> (halfSize arbitraryEnv) <*> (halfSize arbitrary)
                    , (halfSize arbitraryEachT) <*> (halfSize arbitrary)
                    , (halfSize arbitraryEach) <*> (halfSize arbitrary)
                    , Artifact <$> (smaller arbitrary)
                               <*> (smaller arbitrary)
                    , pure EmptyArtifact
                    ]

arbitraryEachT :: Gen ([ArtifactGenerator] -> ArtifactGenerator)
arbitraryEachT = sized $ \n ->
   EachT <$> vectorOf n (halfSize
                            (listOf1
                               (choose ('a', 'z'))))
         <*> oneof [listOf (vectorOf n (halfSize arbitrary))
                   ,listOf1 (listOf (halfSize arbitrary))]

arbitraryEach :: Gen ([ArtifactGenerator] -> ArtifactGenerator)
arbitraryEach = sized $ \n ->
   Each <$> listOf ((,) <$> (listOf1
                               (choose ('a', 'z')))
                        <*> vectorOf n (halfSize
                                          (listOf1
                                             (choose ('a', 'z')))))


instance Arbitrary ArtifactSource where
  arbitrary = oneof [ FromFile <$> smaller arbitraryFilePath
                               <*> smaller arbitrary
                    , FromContent <$> smaller arbitraryFilePath
                                  <*> smaller arbitrary
                    , SetPermissions <$> choose (0,7)
                                     <*> choose (0,7)
                                     <*> choose (0,7)
                                     <*> smaller arbitrary
                    , FromDirectory <$> smaller arbitraryFilePath <*> smaller arbitrary
                    , IntoDirectory <$> smaller arbitraryFilePath <*> smaller arbitrary
                    ]

instance Arbitrary InstanceId where
  arbitrary = IID <$> arbitraryFilePath

instance Arbitrary ArtifactAssembly where
  arbitrary = oneof [ CloudInit <$> arbitrary <*> arbitraryFilePath
                    , pure (VmImages [] NoVmScript)
                    ]

instance Arbitrary CloudInitType where
  arbitrary = elements [CI_ISO,CI_VFAT,CI_DIR]