{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

module Distribution.Types.ForeignLib
  ( ForeignLib (..)
  , emptyForeignLib
  , foreignLibModules
  , foreignLibIsShared
  , foreignLibVersion
  , LibVersionInfo
  , mkLibVersionInfo
  , libVersionInfoCRA
  , libVersionNumber
  , libVersionNumberShow
  , libVersionMajor
  ) where

import Distribution.Compat.Prelude
import Prelude ()

import Distribution.ModuleName
import Distribution.Parsec
import Distribution.Pretty
import Distribution.System
import Distribution.Types.BuildInfo
import Distribution.Types.ForeignLibOption
import Distribution.Types.ForeignLibType
import Distribution.Types.UnqualComponentName
import Distribution.Version

import Data.Monoid
import qualified Distribution.Compat.CharParsing as P
import qualified Text.PrettyPrint as Disp
import qualified Text.Read as Read

import qualified Distribution.Types.BuildInfo.Lens as L

-- | A foreign library stanza is like a library stanza, except that
-- the built code is intended for consumption by a non-Haskell client.
data ForeignLib = ForeignLib
  { foreignLibName :: UnqualComponentName
  -- ^ Name of the foreign library
  , foreignLibType :: ForeignLibType
  -- ^ What kind of foreign library is this (static or dynamic).
  , foreignLibOptions :: [ForeignLibOption]
  -- ^ What options apply to this foreign library (e.g., are we
  -- merging in all foreign dependencies.)
  , foreignLibBuildInfo :: BuildInfo
  -- ^ Build information for this foreign library.
  , foreignLibVersionInfo :: Maybe LibVersionInfo
  -- ^ Libtool-style version-info data to compute library version.
  -- Refer to the libtool documentation on the
  -- current:revision:age versioning scheme.
  , foreignLibVersionLinux :: Maybe Version
  -- ^ Linux library version
  , foreignLibModDefFile :: [FilePath]
  -- ^ (Windows-specific) module definition files
  --
  -- This is a list rather than a maybe field so that we can flatten
  -- the condition trees (for instance, when creating an sdist)
  }
  deriving (Generic, Show, Read, Eq, Ord, Typeable, Data)

data LibVersionInfo = LibVersionInfo Int Int Int deriving (Data, Eq, Generic, Typeable)

instance Ord LibVersionInfo where
  LibVersionInfo c r _ `compare` LibVersionInfo c' r' _ =
    case c `compare` c' of
      EQ -> r `compare` r'
      e -> e

instance Show LibVersionInfo where
  showsPrec d (LibVersionInfo c r a) =
    showParen (d > 10) $
      showString "mkLibVersionInfo "
        . showsPrec 11 (c, r, a)

instance Read LibVersionInfo where
  readPrec = Read.parens $ do
    Read.Ident "mkLibVersionInfo" <- Read.lexP
    t <- Read.step Read.readPrec
    return (mkLibVersionInfo t)

instance Binary LibVersionInfo
instance Structured LibVersionInfo
instance NFData LibVersionInfo where rnf = genericRnf

instance Pretty LibVersionInfo where
  pretty (LibVersionInfo c r a) =
    Disp.hcat $ Disp.punctuate (Disp.char ':') $ map Disp.int [c, r, a]

instance Parsec LibVersionInfo where
  parsec = do
    c <- P.integral
    (r, a) <- P.option (0, 0) $ do
      _ <- P.char ':'
      r <- P.integral
      a <- P.option 0 $ do
        _ <- P.char ':'
        P.integral
      return (r, a)
    return $ mkLibVersionInfo (c, r, a)

-- | Construct 'LibVersionInfo' from @(current, revision, age)@
-- numbers.
--
-- For instance, @mkLibVersionInfo (3,0,0)@ constructs a
-- 'LibVersionInfo' representing the version-info @3:0:0@.
--
-- All version components must be non-negative.
mkLibVersionInfo :: (Int, Int, Int) -> LibVersionInfo
mkLibVersionInfo (c, r, a) = LibVersionInfo c r a

-- | From a given 'LibVersionInfo', extract the @(current, revision,
-- age)@ numbers.
libVersionInfoCRA :: LibVersionInfo -> (Int, Int, Int)
libVersionInfoCRA (LibVersionInfo c r a) = (c, r, a)

-- | Given a version-info field, produce a @major.minor.build@ version
libVersionNumber :: LibVersionInfo -> (Int, Int, Int)
libVersionNumber (LibVersionInfo c r a) = (c - a, a, r)

-- | Given a version-info field, return @"major.minor.build"@ as a
-- 'String'
libVersionNumberShow :: LibVersionInfo -> String
libVersionNumberShow v =
  let (major, minor, build) = libVersionNumber v
   in show major ++ "." ++ show minor ++ "." ++ show build

-- | Return the @major@ version of a version-info field.
libVersionMajor :: LibVersionInfo -> Int
libVersionMajor (LibVersionInfo c _ a) = c - a

instance L.HasBuildInfo ForeignLib where
  buildInfo f l = (\x -> l{foreignLibBuildInfo = x}) <$> f (foreignLibBuildInfo l)

instance Binary ForeignLib
instance Structured ForeignLib
instance NFData ForeignLib where rnf = genericRnf

instance Semigroup ForeignLib where
  a <> b =
    ForeignLib
      { foreignLibName = combineNames a b foreignLibName "foreign library"
      , foreignLibType = combine foreignLibType
      , foreignLibOptions = combine foreignLibOptions
      , foreignLibBuildInfo = combine foreignLibBuildInfo
      , foreignLibVersionInfo = chooseLast foreignLibVersionInfo
      , foreignLibVersionLinux = chooseLast foreignLibVersionLinux
      , foreignLibModDefFile = combine foreignLibModDefFile
      }
    where
      combine field = field a `mappend` field b
      -- chooseLast: the second field overrides the first, unless it is Nothing
      chooseLast field = getLast (Last (field a) <> Last (field b))

instance Monoid ForeignLib where
  mempty =
    ForeignLib
      { foreignLibName = mempty
      , foreignLibType = ForeignLibTypeUnknown
      , foreignLibOptions = []
      , foreignLibBuildInfo = mempty
      , foreignLibVersionInfo = Nothing
      , foreignLibVersionLinux = Nothing
      , foreignLibModDefFile = []
      }
  mappend = (<>)

-- | An empty foreign library.
emptyForeignLib :: ForeignLib
emptyForeignLib = mempty

-- | Modules defined by a foreign library.
foreignLibModules :: ForeignLib -> [ModuleName]
foreignLibModules = otherModules . foreignLibBuildInfo

-- | Is the foreign library shared?
foreignLibIsShared :: ForeignLib -> Bool
foreignLibIsShared = foreignLibTypeIsShared . foreignLibType

-- | Get a version number for a foreign library.
-- If we're on Linux, and a Linux version is specified, use that.
-- If we're on Linux, and libtool-style version-info is specified, translate
-- that field into appropriate version numbers.
-- Otherwise, this feature is unsupported so we don't return any version data.
foreignLibVersion :: ForeignLib -> OS -> [Int]
foreignLibVersion flib Linux =
  case foreignLibVersionLinux flib of
    Just v -> versionNumbers v
    Nothing ->
      case foreignLibVersionInfo flib of
        Just v' ->
          let (major, minor, build) = libVersionNumber v'
           in [major, minor, build]
        Nothing -> []
foreignLibVersion _ _ = []
