{-# OPTIONS -Wall #-}

--------------------------------------------------------------------------------
-- |
-- Module      :  Data.ParserCombinators.Kangaroo.Region
-- Copyright   :  (c) Stephen Tetley 2009-2010
-- License     :  BSD3
--
-- Maintainer  :  Stephen Tetley <stephen.tetley@gmail.com>
-- Stability   :  highly unstable
-- Portability :  to be determined.
--
-- Data types to manage parse regions
--
--------------------------------------------------------------------------------

module Data.ParserCombinators.Kangaroo.Region
  (
  -- * Types
    RegionCoda(..)
  , RegionStart
  , RegionEnd
  , RegionName
  , Pos
  , RegionInfo
  , ParseStack
  , RegionError(..)

  -- * Operations
  , newStack
  , newRegion

  , regionStart
  , regionEnd
  , push
  , pop
  , move1
  , move
  , location

  , printParseStack
  
  ) where


import Text.PrettyPrint.JoinPrint  hiding ( length )


-- | 'RegionCoda' - represents three useful final positions:
--
-- 1. dalpunto  - 'from the point'      
-- - Run the parser within a region and return to where you came
--   from.
--
-- 2. alfermata - 'to the stop'    
-- - Run the parser within a region, the cursor remains wherever 
--   the parse finished.
--
-- 3. alfine    - 'to the end'     
-- - Run the parser within a region and jump to the right-end of 
--   the region after the parse.
--
data RegionCoda = Dalpunto | Alfermata | Alfine
  deriving (Enum,Eq,Show)


type RegionStart = Int
type RegionEnd   = Int
type RegionName  = String

type Pos         = Int

-- | 'RegionInfo' contains the (inclusive) bounds of a region 
-- and the \'coda action\' to take after parsing has finished.
--

data RegionInfo  = RegionInfo 
        { region_start_incl   :: !RegionStart
        , region_end_incl     :: !RegionEnd
        , region_coda         :: !RegionCoda
        , region_name         :: !String
        }
  deriving (Eq,Show)



-- | 'ParseStack' is a non empty list of RegionInfo structures.
--
data ParseStack = P0 Pos RegionInfo | Pn Pos RegionInfo ParseStack
  deriving (Eq,Show)

-- These two might change...

newtype RegionError = RegionError { getRegionError :: String }
  deriving (Eq,Show)



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

mapTop :: (RegionInfo -> a) -> ParseStack -> a
mapTop f (P0 _ info)   = f info
mapTop f (Pn _ info _) = f info

modifyPos :: (Pos -> Pos) -> ParseStack -> ParseStack
modifyPos f (P0 p info)     = P0 (f p) info
modifyPos f (Pn p info stk) = Pn (f p) info stk

validBounds :: RegionStart -> RegionEnd -> ParseStack -> Bool
validBounds s e stk = s >= regionStart stk && e <= regionEnd stk

infos :: ParseStack -> [RegionInfo]
infos (P0 _ info)     = [info]
infos (Pn _ info stk) = info : infos stk

regionError :: RegionName -> RegionStart -> RegionEnd -> ParseStack 
            -> RegionError
regionError nm s e stk = step (regionStart stk) (regionEnd stk) 
  where
    step rs re | s < rs && e > re = mkMsg nm "past the bounds" (s,e) (rs,re)
               | s < rs           = mkMsg nm "past the left"   (s,e) (rs,re)
               | e > re           = mkMsg nm "past the right"  (s,e) (rs,re)
               | otherwise        = RegionError $ 
                                      "regionError called on invalid data."

mkMsg :: RegionName -> String -> (Int,Int) -> (Int,Int) -> RegionError
mkMsg name descr new old  = RegionError $ 
    unwords [ "The new region"
            , ('\'' : name ++ "'")
            , show new
            , "extends"
            , descr
            , "of the old region"
            , show old
            ]

--------------------------------------------------------------------------------
-- Exported operations

regionStart     :: ParseStack -> Int
regionStart     = mapTop region_start_incl

regionEnd       :: ParseStack -> Int
regionEnd       = mapTop region_end_incl


newStack :: RegionStart -> RegionEnd -> RegionCoda -> RegionName -> ParseStack
newStack s e coda name = P0 0 (RegionInfo s e coda name)

newRegion :: RegionStart -> Int -> RegionCoda -> RegionName -> RegionInfo
newRegion s len coda name = RegionInfo s (s+len-1) coda name


push :: RegionInfo -> ParseStack -> Either RegionError ParseStack
push info@(RegionInfo s e _ name) stk 
    | validBounds s e stk = Right $ Pn s info stk
    | otherwise           = Left  $ regionError name s e stk


pop :: ParseStack -> ParseStack
pop (P0 p info)      = P0 p info
pop (Pn p info stk)  = case region_coda info of
    Dalpunto  -> stk
    Alfermata -> modifyPos (const p) stk
    Alfine    -> modifyPos (const $ region_end_incl info) stk

-- Moving will always succeed, so it is possible to move beyond 
-- the end-of-file.
                         
move1 :: ParseStack -> ParseStack
move1 = modifyPos (+1)

move :: (Pos -> Pos) -> ParseStack -> ParseStack
move = modifyPos

location :: ParseStack -> Int
location (P0 p _)   = p
location (Pn p _ _) = p

printParseStack :: ParseStack -> String
printParseStack pstack = unlines $ map (render . fn) stk
  where
    stk             = infos pstack
    (w1,w4)         = onSnd (length . show) $ foldr phi (0,0) stk
    phi info (a,b)  = (max a (length $ region_name info), max b $ region_end_incl info)
    onSnd f (a,b)   = (a, f b)

    fn        :: RegionInfo -> Doc
    fn rgn    =  padl w1 ' ' (text $ region_name rgn)
             <+> padl w4 ' ' (int  $ region_start_incl rgn)
             <+> padl w4 ' ' (int  $ region_end_incl rgn)