{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverlappingInstances #-}

-- | AtomString atomises strings.  Right now this code
-- is not very efficient but it shouldn't be too hard
-- to improve.
--
-- This code includes no less that 3 uses of unsafePerformIO.  Oh well.
module Util.AtomString(
   AtomString,
      -- represents a string.  Instance of Ord, Eq, StringClass,
      -- Read and Show.  There is no guarantee that Ord on AtomString
      -- corresponds to Ord on the corresponding String.
   firstAtomString,
      -- :: AtomString
      -- However firstAtomString is guaranteed to be the first AtomString
      -- in the ordering.

   StringClass(..),
      -- encodes that a type encodes strings in some way.

   fromStringWEHacked,
   fromStringError,
      -- provide a primitive way for decoding String's to return an error.

   Str(..),
      -- WRAP


   mkFromStringWE,
      --  :: Parser stringClass -> String -> (String -> WithError stringClass)
      -- Make a fromStringWE function given a parser.
      -- The error message is of the form "/string/ is not a valid /typename/"
      -- where /typename/ is the first String argument to mkFromStringWE.
   ) where

import Control.Concurrent
import qualified Data.Map as Map
import System.IO.Unsafe
import qualified Data.ByteString.Char8 as BS
import Control.Exception
import Text.ParserCombinators.Parsec


import Util.QuickReadShow
import Util.Dynamics
import Util.DeepSeq
import Util.Computation
import Util.BinaryAll

data AtomSource = AtomSource (MVar (Map.Map BS.ByteString AtomString))
   -- where AtomStrings come from
   -- Here the key for an element is itself.


emptyAtomSource :: IO AtomSource
emptyAtomSource =
   do
      mVar <- newMVar Map.empty
      return (AtomSource mVar)

theAtomSource :: AtomSource
theAtomSource = unsafePerformIO emptyAtomSource
{-# NOINLINE theAtomSource #-}
-- avoid GHC bug with Linux optimisation which can clone MVars.

newtype AtomString = AtomString BS.ByteString deriving (Ord,Eq,Typeable)
-- in fact Eq could be unsafePtrEq

firstAtomString :: AtomString
firstAtomString = AtomString (BS.pack "")

------------------------------------------------------------------------
-- StringClass
------------------------------------------------------------------------

class StringClass stringClass where
   toString :: stringClass -> String

   -- We leave it up to the instance whether fromString or fromStringWE or both
   -- are defined.  Most of the time we only use fromString, but there are
   -- just a few cases (such as EntityNames) where we need fromStringWE.
   --
   -- For cases where we don't have fromStringWE fromStringWEHacked provides
   -- an alternative solution, if you can bear it.
   fromString :: String -> stringClass
   fromString s = coerceWithError (fromStringWE s)

   fromStringWE :: String -> WithError stringClass
   fromStringWE s = hasValue (fromString s)

instance StringClass AtomString where
   fromString string = unsafePerformIO (mkAtom string)

   toString atom = unsafePerformIO (readAtom atom)

instance StringClass stringClass => QuickRead stringClass where
   quickRead = WrapRead fromString

instance StringClass stringClass => QuickShow stringClass where
   quickShow = WrapShow toString

------------------------------------------------------------------------
-- We provide a way for instances of StringClass to return errors from
-- fromString by using the usual dreadful hack with Exception.
------------------------------------------------------------------------

fromStringWEHacked :: (StringClass stringClass,DeepSeq stringClass)
   => String -> IO (WithError stringClass)
fromStringWEHacked str =
   do
      either <- tryJust
         (\ dyn ->
               case fromDynamic dyn of
                  Nothing -> Nothing -- not a fromStringError.
                  Just (FromStringExcep mess) -> Just mess
            )
         (do
             let
                value = fromString str
             deepSeq value done
             return value
         )
      return (toWithError either)

fromStringError :: String -> a
fromStringError mess = throw $ toDyn (FromStringExcep mess)

newtype FromStringExcep = FromStringExcep String deriving (Typeable)

------------------------------------------------------------------------
-- StringClass instance
------------------------------------------------------------------------

mkAtom :: String -> IO AtomString
mkAtom str =
   do
      let
         packed = BS.pack str
         AtomSource mVar = theAtomSource

      map <- takeMVar mVar
      let
         (result,newMap) = case Map.lookup packed map of
            Nothing ->
               (AtomString packed,Map.insert packed (AtomString packed) map)
            Just newPacked -> (newPacked,map)
            -- now original copy of packed can be GC'd.
      putMVar mVar newMap
      return result


readAtom :: AtomString -> IO String
readAtom (AtomString packedString) =
   return(BS.unpack packedString)

------------------------------------------------------------------------
-- How to make a fromStringWE given a Parsec parser.
------------------------------------------------------------------------

mkFromStringWE :: Parser stringClass -> String
   -> (String -> WithError stringClass)
mkFromStringWE (parser0 :: Parser stringClass) typeName str =
   let
      parser1 =
         do
            result <- parser0
            eof
            return result
   in
      case parse parser1 "" str of
         Right stringClass -> hasValue stringClass
         Left _ -> hasError (show str ++ " is not a valid " ++ typeName)

------------------------------------------------------------------------
-- The Str class.  Wrapping an instance of StringClass in this gives
-- you an instance of HasBinary.
------------------------------------------------------------------------

newtype Str a = Str a

instance (Monad m,StringClass a) => HasBinary (Str a) m where
   writeBin = mapWrite (\ (Str a) -> toString a)
   readBin = mapRead (\ str -> Str (fromString str))