-- | Reading 'Symbols' from and writing to interface files
{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-name-shadowing #-}
module Language.Haskell.Names.Interfaces
  (
  -- * High-level interface
    NamesDB(..)
  , runNamesModuleT
  , evalNamesModuleT
  -- * Low-level interface
  , readInterface
  , writeInterface
  -- * Exceptions
  , IfaceException(..)
  ) where

import Language.Haskell.Names.Types
import Language.Haskell.Exts (ModuleName(ModuleName),prettyPrint,Name)
import Language.Haskell.Names.SyntaxUtils (stringToName,nameToString,annName)
import Language.Haskell.Exts.Annotated.Simplify (sName)
import qualified Data.ByteString.Lazy as BS
import Data.Aeson
import Data.Monoid
import Data.Char
import Data.Typeable
import qualified Data.Map as Map
import Control.Exception
import Control.Applicative
import Control.Monad
import Distribution.HaskellSuite
import qualified Distribution.ModuleName as Cabal
import System.FilePath

import Paths_haskell_names

data IfaceException =
  -- | Interface could not be parsed. This tells you the file name of the
  -- interface file and the parse error text.
  BadInterface FilePath String
  deriving (Typeable, Show)
instance Exception IfaceException

-- | Read an interface file
readInterface :: FilePath -> IO [Symbol]
readInterface path =
  either (throwIO . BadInterface path) return =<<
    eitherDecode <$> BS.readFile path

-- | Write an interface file
writeInterface :: FilePath -> [Symbol] -> IO ()
writeInterface path iface =
  BS.writeFile path $
    encode iface `mappend` BS.pack [fromIntegral $ ord '\n']

prettyName :: Name -> String
prettyName = nameToString . annName

instance ToJSON Symbol where
  toJSON symbol =
    object ([
      "entity" .= symbolEntity symbol,
      "module" .= prettyPrint (symbolModule symbol),
      "name" .= prettyName (symbolName symbol)] ++ additionalInfo symbol)
    where
      additionalInfo symbol = case symbol of
        Method { className = cls } ->
          ["class" .= prettyName cls]
        Selector { typeName = ty, constructors = cons } ->
          ["type" .= prettyName ty
          ,"constructors".= map prettyName cons]
        Constructor { typeName = ty } ->
          ["type".= prettyName ty]
        _ -> []

symbolEntity :: Symbol -> String
symbolEntity i = case i of
  Value {} -> "value"
  Method {} -> "method"
  Selector {} -> "selector"
  Constructor {} -> "constructor"
  Type {} -> "type"
  Data {} -> "data"
  NewType {} -> "newtype"
  TypeFam {} -> "typeFamily"
  DataFam {} -> "dataFamily"
  Class   {} -> "class"

parseName :: String -> Name
parseName = sName . stringToName

instance FromJSON Symbol where
  parseJSON (Object v) = do
    entity <- v .: "entity"
    symbolmodule <- ModuleName <$> v .: "module"
    symbolname <- parseName <$> v .: "name"

    case entity :: String of
      "value" -> return $ Value symbolmodule symbolname
      "method" -> do
        cls <- v .: "class"
        return (Method symbolmodule symbolname (parseName cls))
      "selector" -> do
        typ <- v .: "type"
        cons <- v .: "constructors"
        return (Selector symbolmodule symbolname (parseName typ) (map parseName cons))
      "constructor" -> do
        typ <- v .: "type"
        return (Constructor symbolmodule symbolname (parseName typ))
      "type" -> return $ Type symbolmodule symbolname
      "data" -> return $ Data symbolmodule symbolname
      "newtype" -> return $ NewType symbolmodule symbolname
      "typeFamily" -> return $ TypeFam symbolmodule symbolname
      "dataFamily" -> return $ DataFam symbolmodule symbolname
      "class" -> return $ Class symbolmodule symbolname
      _ -> mzero

  parseJSON _ = mzero

-- | The database used by @hs-gen-iface@. Use it together with
-- functions from "Distribution.HaskellSuite.Packages".
newtype NamesDB = NamesDB FilePath

instance IsPackageDB NamesDB where
  dbName = return "haskell-names"
  readPackageDB init (NamesDB db) =
    map (makePkgInfoAbsolute (dropFileName db)) <$> readDB init db
  writePackageDB (NamesDB db) = writeDB db
  globalDB = Just . NamesDB . (</> "libraries" </> "packages.db") <$> getDataDir
  dbFromPath path = return $ NamesDB path

-- | Extension of the name files (i.e. @"names"@)
nameFilesExtension :: FilePath
nameFilesExtension = "names"

-- | Specialized version of 'runModuleT' that works with name files
runNamesModuleT
  :: ModuleT [Symbol] IO a
  -> Packages
  -> Map.Map Cabal.ModuleName [Symbol]
  -> IO (a, Map.Map Cabal.ModuleName [Symbol])
runNamesModuleT ma pkgs = runModuleT ma pkgs nameFilesExtension readInterface

-- | Specialized version of 'evalModuleT' that works with name files
evalNamesModuleT
  :: ModuleT [Symbol] IO a
  -> Packages
  -> IO a
evalNamesModuleT ma pkgs = evalModuleT ma pkgs nameFilesExtension readInterface