-- Copyright (c) 2014 Eric McCorkle.  All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
--
-- 1. Redistributions of source code must retain the above copyright
--    notice, this list of conditions and the following disclaimer.
--
-- 2. Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
--
-- 3. Neither the name of the author nor the names of any contributors
--    may be used to endorse or promote products derived from this software
--    without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS''
-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
-- TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-- PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS
-- OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
-- USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
-- OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
{-# OPTIONS_GHC -Wall -Werror -funbox-strict-fields #-}
{-# LANGUAGE DeriveDataTypeable #-}

-- | Utilities for constructing enumerations over datatypes.
--
-- An 'Enumeration' is a mapping between a particular datatype and a
-- 'Path', which consists of a list of natural numbers.  Conceptually,
-- we can think of all the values of a given datatype as being
-- organized into a tree, with nodes representing decisions that
-- narrow down the choices.  In such a scheme, a 'Path' represents a
-- path through the tree to a single value, and an 'Enumeration' is a
-- procedure for converting a 'Path' to and from a value.
--
--
-- An 'Enumeration' has two key functions: 'fromPath' and 'toPath',
-- which translate between paths and instances of a datatype.  These
-- functions are expected to be inverses; or:
--
--   * @fromPath (toPath v) == v@ for all values in the domain
--
-- Beyond this, there are no additional restrictions.  Specifically,
-- two paths /may/ map to the same value.
--
--
-- The 'numBranches' function indicates the maximum value of the first
-- path element for an 'Enumeration'.  The minimum value is always 0,
-- and all values between 0 and 'numBranches' must be valid.  If there
-- is no upper bound on the value of the first path element,
-- 'numBranches' returns 'Nothing'.  The 'toSizedPath' maps a value to
-- a path, which also contains the result of 'numBranches' at each
-- step in the path.
--
--
-- The 'withPrefix' function supplies a partial path to an
-- 'Enumeration', yielding a new 'Enumeration' that maps each value to
-- the same path(s) as the original 'Enumeration', with the prefix
-- added or removed.  More formally, if
--
--   * @subenc = withPrefix enc prepath@
--
-- Then
--
--   * @toPath enc val == prepath ++ toPath subenc val@
--
--   * @fromPath enc (prepath ++ subpath) == fromPath subenc subpath@
--
-- With multiple uses of 'withPrefix', the following must be true:
--
--   * @withPrefix (withPrefix enc path1) path2 == withPrefix enc (path1 ++ path2)@
--
-- Finally, if a complete path is given to 'withPrefix', then the
-- result is a singleton encoding that gives the value associated with
-- that path.  That is:
--
--   * @fromPath enc fullpath == fromPath (withPrefix enc fullpath) []@
--
-- This provides \"warm-start\" functionality for 'Enumeration's.
-- When translating a large number of 'Path's with the same prefix, it
-- will generally be much more efficient to use 'withPrefix' and use
-- the resulting 'Enumeration' than to translate the 'Path's directly.
--
--
-- The 'prefix' function gives the current prefix for an
-- 'Enumeration'.  The following rule describes the relationship
-- between 'prefix' and 'withPrefix':
--
--   * @prefix (withPrefix enc prepath) == prepath@
--
--
-- 'Enumeration's are similar to 'Encoding's from the arith-encode
-- library, except 'Enumeration's are generally more flexible, and can
-- more easily accomodate complex datatypes with invariants.  However,
-- as 'Path's are constructed from natural numbers, we can create an
-- 'Enumeration' using a series of 'Encoding's for intermediate data.
-- The functions in this module provide the ability to construct
-- 'Enumeration's using 'Encoding's
--
-- A singleton 'Enumeration' can be constructed using the 'singleton'
-- and 'singletonWithPrefix' functions.
--
-- An 'Encoding' for a datatype can be converted into an 'Enumeration'
-- (where all paths have a single element) using the 'fromEncoding'
-- and 'fromEncodingWithPrefix' functions.
--
-- The 'step' and 'stepWithPrefix' functions construct an
-- 'Enumeration' from an 'Encoding' for an intermediate value, and a
-- generator function that produces an 'Enumeration' from the
-- intermediate value.
--
-- The @withPrefix@ variants for each of these take a prefix path,
-- where the non-@withPrefix@ variants set the prefix to @[]@.
module Data.Enumeration(
       -- * Definitions
       Enumeration,
       Path,
       BadPath(..),
       IllegalArgument(..),

       -- ** Using Enumerations
       fromPath,
       toPath,
       toSizedPath,
       withPrefix,
       numBranches,
       prefix,

       -- * Constructions
       singleton,
       singletonWithPrefix,
       fromEncoding,
       fromEncodingWithPrefix,
       step,
       stepWithPrefix
       ) where

import Control.Exception
import Data.List
import Data.ArithEncode hiding (singleton)
import Data.Typeable

-- | A path that uniquely identifies a value in an @Enumeration@.
type Path = [Integer]

-- | A datatype that represents a mapping between @Path@s and @ty@s.
-- Note that unlike @Encoding@s, not all @Path@s are necessarily
-- valid.
data Enumeration ty =
  Enumeration {
    -- | Convert a @ty@ to a @Path@
    toPath :: !(ty -> Path),
    -- | Convert to a list of pairs, where the @fst@ holds
    -- the path entry, and @snd@ holds @numBranches@.  This is used
    -- primarily for encoding values as binary.
    toSizedPath :: !(ty -> [(Integer, Maybe Integer)]),
    -- | Generate a @ty@ from a @Path@
    fromPath :: !(Path -> ty),
    -- | Given a prefix path, get an enumeration that generates @ty@s
    -- from the rest of the path.
    withPrefix :: !(Path -> Enumeration ty),
    -- | Get the upper bound on values for the first path component,
    -- or @Nothing@ if there is no bound.
    numBranches :: !(Maybe Integer),
    -- | The prefix path.
    prefix :: !Path
  }

-- | An exception thrown when a 'Path' is invalid.
data BadPath = BadPath String
  deriving Typeable

instance Show BadPath where
  show (BadPath "") = "Bad Path"
  show (BadPath msg) = "Bad Path: " ++ msg

instance Exception BadPath

showPath :: Path -> String
showPath = intercalate "." . map show

-- | Create an 'Enumeration' with an empty prefix that maps a single
-- value to and from the empty path.  Equivalent to
-- @singletonWithPrefix []@
singleton :: Eq ty =>
             ty
          -- ^ The value to map to and from the empty path.
          -> Enumeration ty
singleton = singletonWithPrefix []

-- | Create an 'Enumeration' with a given prefix path that maps a
-- single value to and from the empty path.
singletonWithPrefix :: Eq ty => Path -> ty -> Enumeration ty
singletonWithPrefix prefixPath val =
  let
    showCompletePath path = showPath (prefixPath ++ path)

    fromPathFunc [] = val
    fromPathFunc path =
      throw $! BadPath $! "Extra path elements " ++ showCompletePath path

    toPathFunc val'
      | val' == val = []
      | otherwise = throw $! IllegalArgument "Bad argument to singleton"

    toSizedPathFunc val'
      | val' == val = []
      | otherwise = throw $! IllegalArgument "Bad argument to singleton"

    withPrefixFunc [] = out
    withPrefixFunc path =
      throw $! BadPath $! "Extra path elements " ++ showCompletePath path

    out = Enumeration { fromPath = fromPathFunc, toPath = toPathFunc,
                        withPrefix = withPrefixFunc, numBranches = Just 0,
                        prefix = prefixPath, toSizedPath = toSizedPathFunc }
  in
    out

-- | Create an 'Enumeration' with an empty prefix from a single
-- 'Encoding'.  The 'Path' will always be of length 1, and contains
-- the encoded value.
fromEncoding :: Eq ty =>
                Encoding ty
             -- ^ The 'Encoding' to use
             -> Enumeration ty
fromEncoding = fromEncodingWithPrefix []

-- | Create an 'Enumeration' with a given prefix from a single
-- 'Encoding'.  The 'Path' will always be of length 1, and contains
-- the encoded value.
fromEncodingWithPrefix :: Eq ty => Path -> Encoding ty -> Enumeration ty
fromEncodingWithPrefix prefixPath enc =
  let
    fromPathFunc [encoded] = decode enc encoded
    fromPathFunc [] = throw $! BadPath "Path too short"
    fromPathFunc (_ : path) =
      throw $! BadPath $! "Extra path elements " ++ showPath path

    toPathFunc val = [encode enc val]
    toSizedPathFunc val = [(encode enc val, size enc)]

    withPrefixFunc newPrefix @ [encoded] =
      singletonWithPrefix (prefixPath ++ newPrefix) (decode enc encoded)
    withPrefixFunc [] = out
    withPrefixFunc (_ : path) =
      throw $! BadPath $! "Extra path elements " ++ showPath path

    out = Enumeration { fromPath = fromPathFunc, toPath = toPathFunc,
                        withPrefix = withPrefixFunc, numBranches = size enc,
                        prefix = prefixPath, toSizedPath = toSizedPathFunc }
  in
    out

-- | Create an 'Enumeration' with an empty prefix that uses an
-- 'Encoding' to convert the first element of the path to an interim
-- value, then uses that value to construct an 'Enumeration' to decode
-- the rest of the path.
step :: Encoding ty1
     -- ^ The encoding for the first type.
     -> (Path -> ty1 -> Enumeration ty2)
     -- ^ A function that produces an enumeration from the first type.
     -> (ty2 -> ty1)
     -- ^ A function that extracts the first type from the second.
     -> Enumeration ty2
step = stepWithPrefix []

-- | Create an 'Enumeration' with a prefix that uses an 'Encoding' to
-- convert the first element of the path to an interim value, then
-- uses that value to construct an 'Enumeration' to decode the rest of
-- the path.
stepWithPrefix :: Path
               -- ^ The prefix path.
               -> Encoding ty1
               -- ^ The 'Encoding' for the first type.
               -> (Path -> ty1 -> Enumeration ty2)
               -- ^ A function that produces an enumeration from the first type.
               -> (ty2 -> ty1)
               -- ^ A function that extracts the first type from the second.
               -> Enumeration ty2
stepWithPrefix prefixPath enc build extract =
  let
    fromPathFunc (first : rest) =
      fromPath (build prefixPath (decode enc first)) rest
    fromPathFunc [] = throw $! BadPath "Path too short"

    toPathFunc val =
      let
        extracted = extract val
        inner = build prefixPath extracted
      in
        encode enc extracted : toPath inner val

    toSizedPathFunc val =
      let
        extracted = extract val
        inner = build prefixPath extracted
      in
        (encode enc extracted, size enc) : toSizedPath inner val

    withPrefixFunc (first : rest) =
      let
        extracted = decode enc first
        newPrefix = prefixPath ++ [first]
        inner = build newPrefix extracted
      in
       withPrefix inner rest
    withPrefixFunc [] = out

    out = Enumeration { fromPath = fromPathFunc, toPath = toPathFunc,
                        withPrefix = withPrefixFunc, numBranches = size enc,
                        prefix = prefixPath, toSizedPath = toSizedPathFunc }
  in
    out