-- -*- coding: utf-8; mode: haskell; -*-

-- File: library/Language/Ninja/IR/Target.hs
--
-- License:
--     Copyright 2017 Awake Security
--
--     Licensed under the Apache License, Version 2.0 (the "License");
--     you may not use this file except in compliance with the License.
--     You may obtain a copy of the License at
--
--       http://www.apache.org/licenses/LICENSE-2.0
--
--     Unless required by applicable law or agreed to in writing, software
--     distributed under the License is distributed on an "AS IS" BASIS,
--     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--     See the License for the specific language governing permissions and
--     limitations under the License.

{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE UndecidableInstances       #-}

-- |
--   Module      : Language.Ninja.IR.Target
--   Copyright   : Copyright 2017 Awake Security
--   License     : Apache-2.0
--   Maintainer  : opensource@awakesecurity.com
--   Stability   : experimental
--
--   Types relating to Ninja build targets, outputs, and dependencies.
--
--   @since 0.1.0
module Language.Ninja.IR.Target
  ( -- * @Target@
    Target, makeTarget, targetIText, targetText

    -- * @Output@
  , Output, makeOutput, outputTarget

    -- * @Dependency@
  , Dependency, makeDependency, dependencyTarget, dependencyType
  , DependencyType (..), _NormalDependency, _OrderOnlyDependency
  ) where

import qualified Control.Lens              as Lens

import           Data.Text                 (Text)
import qualified Data.Text                 as Text

import           Data.Aeson                ((.:), (.=))
import qualified Data.Aeson                as Aeson

import           Control.DeepSeq           (NFData)
import           Data.Hashable             (Hashable)
import           Data.String               (IsString)
import           GHC.Generics              (Generic)

import           Test.SmallCheck.Series    ((>>-))
import qualified Test.SmallCheck.Series    as SC

import qualified Language.Ninja.Misc.IText as Misc

import           Flow                      ((.>), (|>))

--------------------------------------------------------------------------------

-- | This type represents a Ninja target name.
--
--   @since 0.1.0
newtype Target
  = MkTarget
    { _targetIText :: Misc.IText
    }
  deriving ( Eq, Ord, Show, Read, IsString, Generic, Hashable, NFData
           , Aeson.ToJSON, Aeson.FromJSON, Aeson.ToJSONKey, Aeson.FromJSONKey )

-- | Construct a 'Target' from some 'Text'.
--
--   @since 0.1.0
{-# INLINE makeTarget #-}
makeTarget :: Text -> Target
makeTarget = Lens.view Misc.itext .> MkTarget

-- | An isomorphism between a 'Target' and its underlying 'IText'.
--
--   @since 0.1.0
{-# INLINE targetIText #-}
targetIText :: Lens.Iso' Target Misc.IText
targetIText = Lens.iso _targetIText MkTarget

-- | An isomorphism that gives access to a 'Text'-typed view of a 'Target',
--   even though the underlying data has type 'IText'.
--
--   This is equivalent to @targetIText . from itext@.
--
--   @since 0.1.0
{-# INLINE targetText #-}
targetText :: Lens.Iso' Target Text
targetText = targetIText . Lens.from Misc.itext

-- | Uses the underlying 'IText' instance.
--
--   @since 0.1.0
instance (Monad m, SC.Serial m Text) => SC.Serial m Target where
  series = SC.newtypeCons MkTarget

-- | Uses the underlying 'IText' instance.
--
--   @since 0.1.0
instance (Monad m, SC.CoSerial m Text) => SC.CoSerial m Target where
  coseries rs = SC.newtypeAlts rs
                >>- \f -> pure (_targetIText .> f)

--------------------------------------------------------------------------------

-- | A Ninja build output.
--
--   More information is available
--   <https://ninja-build.org/manual.html#ref_outputs here>.
--
--   @since 0.1.0
newtype Output
  = MkOutput
    { _outputTarget :: Target
    }
  deriving (Eq, Ord, Show, Read, Generic)

-- | Construct an 'Output'.
--
--   @since 0.1.0
{-# INLINE makeOutput #-}
makeOutput :: Target
           -- ^ The underlying target.
           -> Output
makeOutput = MkOutput

-- | A lens for the 'Target' of an 'Output'.
--
--   @since 0.1.0
{-# INLINE outputTarget #-}
outputTarget :: Lens.Lens' Output Target
outputTarget = Lens.lens _outputTarget
               $ \(MkOutput {..}) new -> MkOutput { _outputTarget = new, .. }

-- | Converts to @{target: …, type: …}@.
--
--   @since 0.1.0
instance Aeson.ToJSON Output where
  toJSON (MkOutput {..})
    = [ "target" .= _outputTarget
      ] |> Aeson.object

-- | Inverse of the 'Aeson.ToJSON' instance.
--
--   @since 0.1.0
instance Aeson.FromJSON Output where
  parseJSON = (Aeson.withObject "Output" $ \o -> do
                  _outputTarget <- (o .: "target") >>= pure
                  pure (MkOutput {..}))

-- | Default 'Hashable' instance via 'Generic'.
--
--   @since 0.1.0
instance Hashable Output

-- | Default 'NFData' instance via 'Generic'.
--
--   @since 0.1.0
instance NFData Output

-- | Default 'SC.Serial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m, SC.Serial m Text) => SC.Serial m Output

-- | Default 'SC.CoSerial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m, SC.CoSerial m Text) => SC.CoSerial m Output

--------------------------------------------------------------------------------

-- | A build dependency.
--
--   More information is available
--   <https://ninja-build.org/manual.html#ref_dependencies here>.
--
--   @since 0.1.0
data Dependency
  = MkDependency
    { _dependencyTarget :: !Target
    , _dependencyType   :: !DependencyType
    }
  deriving (Eq, Ord, Show, Read, Generic)

-- | Construct a 'Dependency'.
--
--   @since 0.1.0
{-# INLINE makeDependency #-}
makeDependency :: Target
               -- ^ The underlying target.
               -> DependencyType
               -- ^ The dependency type (normal, implicit, or order-only).
               -> Dependency
makeDependency = MkDependency

-- | A lens for the 'Target' of a 'Dependency'.
--
--   @since 0.1.0
{-# INLINE dependencyTarget #-}
dependencyTarget :: Lens.Lens' Dependency Target
dependencyTarget
  = Lens.lens _dependencyTarget
    $ \(MkDependency {..}) new -> MkDependency { _dependencyTarget = new, .. }

-- | A lens for the 'DependencyType' of a 'Dependency'.
--
--   @since 0.1.0
{-# INLINE dependencyType #-}
dependencyType :: Lens.Lens' Dependency DependencyType
dependencyType
  = Lens.lens _dependencyType
    $ \(MkDependency {..}) new -> MkDependency { _dependencyType = new, .. }

-- | Converts to @{target: …, type: …}@.
--
--   @since 0.1.0
instance Aeson.ToJSON Dependency where
  toJSON (MkDependency {..})
    = [ "target" .= _dependencyTarget
      , "type"   .= _dependencyType
      ] |> Aeson.object

-- | Inverse of the 'Aeson.ToJSON' instance.
--
--   @since 0.1.0
instance Aeson.FromJSON Dependency where
  parseJSON = (Aeson.withObject "Dependency" $ \o -> do
                  _dependencyTarget <- (o .: "target") >>= pure
                  _dependencyType   <- (o .: "type")   >>= pure
                  pure (MkDependency {..}))

-- | Default 'Hashable' instance via 'Generic'.
--
--   @since 0.1.0
instance Hashable Dependency

-- | Default 'NFData' instance via 'Generic'.
--
--   @since 0.1.0
instance NFData Dependency

-- | Default 'SC.Serial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m, SC.Serial m Text) => SC.Serial m Dependency

-- | Default 'SC.CoSerial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m, SC.CoSerial m Text) => SC.CoSerial m Dependency

--------------------------------------------------------------------------------

-- | The type of a 'Dependency': normal, implicit, or order-only.
--
--   @since 0.1.0
data DependencyType
  = -- | A normal dependency.
    --
    --   @since 0.1.0
    NormalDependency
  | -- | An order-only dependency. These are only rebuilt if there is at least
    --   one non-order-only dependency that is out of date.
    --
    --   Section 4.3 "Types of Prerequisites" in the GNU Make manual has a good
    --   explanation of this concept.
    --
    --   @since 0.1.0
    OrderOnlyDependency
  deriving (Eq, Ord, Show, Read, Generic)

-- | A prism for the 'NormalDependency' constructor.
--
--   @since 0.1.0
{-# INLINE _NormalDependency #-}
_NormalDependency :: Lens.Prism' DependencyType ()
_NormalDependency = Lens.prism' (const NormalDependency)
                    $ \case NormalDependency -> Just ()
                            _                -> Nothing

-- | A prism for the 'OrderOnlyDependency' constructor.
--
--   @since 0.1.0
{-# INLINE _OrderOnlyDependency #-}
_OrderOnlyDependency :: Lens.Prism' DependencyType ()
_OrderOnlyDependency = Lens.prism' (const OrderOnlyDependency)
                       $ \case OrderOnlyDependency -> Just ()
                               _                   -> Nothing

-- | Converts to @"normal"@, @"implicit"@, and @"order-only"@ respectively.
--
--   @since 0.1.0
instance Aeson.ToJSON DependencyType where
  toJSON NormalDependency    = "normal"
  toJSON OrderOnlyDependency = "order-only"

-- | Inverse of the 'Aeson.ToJSON' instance.
--
--   @since 0.1.0
instance Aeson.FromJSON DependencyType where
  parseJSON = (Aeson.withText "DependencyType" $ \case
                  "normal"     -> pure NormalDependency
                  "order-only" -> pure OrderOnlyDependency
                  owise        -> [ "Invalid dependency type "
                                  , "\"", owise, "\"; should be one of "
                                  , "[\"normal\", \"order-only\"]"
                                  ] |> mconcat |> Text.unpack |> fail)

-- | Default 'Hashable' instance via 'Generic'.
--
--   @since 0.1.0
instance Hashable DependencyType

-- | Default 'NFData' instance via 'Generic'.
--
--   @since 0.1.0
instance NFData DependencyType

-- | Default 'SC.Serial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m) => SC.Serial m DependencyType

-- | Default 'SC.CoSerial' instance via 'Generic'.
--
--   @since 0.1.0
instance (Monad m) => SC.CoSerial m DependencyType

--------------------------------------------------------------------------------