{-# LANGUAGE NamedFieldPuns #-}
module Codec.Game.Puz 
       (Style (Plain,Circle), Square (Black,Letter,Rebus), 
        Dir (Across,Down), Puzzle (Puzzle), Index,
        width,height,grid,solution,title,author,notes,
        copyright,timer,clues,locked,
        numberGrid,unlockPuz,bruteForceUnlockPuz,
        loadPuzzle,savePuzzle,stringCksum)
where

import Codec.Game.Puz.Internal

import System.IO hiding (hGetContents)
import System.IO.Error

import Foreign.Ptr
import Foreign.C
import Foreign.Marshal.Array

import Data.ByteString hiding (map,foldl,foldl',zip,zipWith,length,find,
                               all,any,reverse,putStrLn,replicate)
import Data.Array
import Data.List
import Data.Maybe

import Control.Monad

{- ------ Types ------- -}

{-| The 'Style' type enumerates the possible styles of fillable squares.
    Currently, there are only two: plain squares and circled squares. -}
data Style = Plain | Circle
  deriving (Eq, Show)

{-| The 'Square' type represents a square in a puzzle. -}
data Square
    -- | Black squares
    = Black
    -- | Standard letter squares, optionally filled in
    | Letter (Maybe Char) Style
    -- | Rebus squares, optionally filled in
    | Rebus String Style
  deriving (Eq, Show)

data Dir = Across | Down
  deriving (Eq, Show)

type Index = (Int,Int)

{-| The 'Puzzle' type represents a particular crossword.  The
    crossword's dimensions are specified by 'width' and 'height'.

    The contents of the puzzle are given by two arrays of 'Square's -
    'grid' and 'solution'.  The board arrays are in row-major order
    and are numbered from (0,0) to (width-1,height-1).  The 'grid'
    board represents the current state of play, and as such its
    squares may be partially or entirely filled in, correctly or
    incorrectly.  The 'solution' board should have the same basic
    layout as the 'grid' board (in terms of black vs letter squares),
    and should be entirely filled in.

    Various other pieces of data about the puzzle are given bu
    'title', 'author', 'notes' and 'copyright', all 'String's.

    There is an optional "timer", which is a number of seconds
    elapsed and a bool that is true if the timer is stopped and
    false otherwise.

    The field 'clues' gives the puzzle's clues.  The numbers in this
    array correspond to the numbering that would appear on the grid.
    To reconstruct this information, see the 'numberGrid' function.
 -}
data Puzzle =
  Puzzle { width,height                  :: Int,
           grid,solution                 :: Array Index Square,
           title,author,notes,copyright  :: String,
           timer                         :: Maybe (Int,Bool),
           clues                         :: [(Int,Dir,String)],
           locked                        :: Maybe CUShort
          }
  deriving (Show)

type ErrMsg = String

{- ------- Constants ------- -}
blankChar,blackChar,extrasBlankChar :: CUChar
blackChar = fromIntegral (fromEnum '.')
blankChar = fromIntegral (fromEnum '-')
extrasBlankChar = toEnum 0

styleMap :: [(CUChar,Style)]
styleMap  = [(0,Plain),(128,Circle)]
styleMap' :: [(Style,CUChar)]
styleMap' = map (\(a,b) -> (b,a)) styleMap

charToStyle :: CUChar -> Maybe Style
charToStyle i = lookup i styleMap

styleToChar :: Style -> CUChar
styleToChar s = fromJust $ lookup s styleMap'


-- how to order clues 
orderClues :: (Int,Dir,String) -> (Int,Dir,String) -> Ordering
orderClues (i1,d1,_) (i2,d2,_) = 
  case compare i1 i2 of
    EQ -> case (d1,d2) of
            (Across,Down) -> LT
            (Down,Across) -> GT
            _ -> EQ
    c -> c

{- ---- Internal marshalling stuff ---- -}

cucharToChar :: CUChar -> Char
cucharToChar = toEnum . fromEnum

-- The bool is true of this is a game board and false if it is a solution
-- board
charToSquare :: Bool -> [(Int,String)] -> CUChar -> CUChar -> CUChar
             -> Maybe String -> Square
charToSquare isGame rtbl sq rbs ext rusr =
    if sq == blackChar then Black 
    else
      if isGame then
         case (rusr,rebus) of
           (Just str, _) -> Rebus str style
           (_, Just _)   -> 
              let str' = if sq == blankChar then [] else [cucharToChar sq] 
              in Rebus str' style
           (_,_)         -> dflt
      else
         case rebus of
           Just str -> Rebus str style
           Nothing  -> dflt
  where
    style = case charToStyle ext of
              Just s  -> s
              Nothing -> Plain --XXX maybe I should issue some kind of warning
    
    rebus = if rbs == 0 then Nothing else
              case lookup (fromIntegral rbs) rtbl of
                     Nothing -> error ("Puzzle file contains ill-formed " ++
                                       "rebus section")
                     Just str -> Just str

    dflt  =  if sq == blankChar then Letter Nothing style
                                else Letter (Just $ cucharToChar sq) style

squareToBoardChar :: Square -> CUChar
squareToBoardChar Black        = blackChar
squareToBoardChar (Letter m _) = case m of
                                   Nothing -> blankChar
                                   Just c  -> charToCUChar c
squareToBoardChar (Rebus m _)  = case m of
                                   []    -> blankChar
                                   (c:_) -> charToCUChar c

squareToExtrasChar :: Square -> CUChar
squareToExtrasChar Black        = styleToChar Plain
squareToExtrasChar (Letter _ s) = styleToChar s
squareToExtrasChar (Rebus _ s)  = styleToChar s

gridToExtras :: [Square] -> Maybe [CUChar]
gridToExtras sqs = 
    let es = map squareToExtrasChar sqs
        ps = styleToChar Plain
    in if all (ps==) es then Nothing else Just es

gridToRebus :: [Square] -> Maybe ([(String,Int)],[CUChar])
gridToRebus sqs = 
    case foldl folder (0,[],[]) sqs of
      (0,_,_)     -> Nothing
      (_,rtbl,is) -> Just (reverse rtbl, reverse is)
    where
      folder :: (Int,[(String,Int)],[CUChar]) -> Square ->
                (Int,[(String,Int)],[CUChar])
      folder (n,rtbl,is) sq = 
          case sq of
            Black      -> (n,rtbl,extrasBlankChar:is)
            Letter _ _ -> (n,rtbl,extrasBlankChar:is)
            Rebus s _  -> case lookup s rtbl of
                            Nothing -> (n+1, (s,n):rtbl, (toEnum (n+1)):is)
                            Just n' -> (n, rtbl, (toEnum (n'+1)):is)

gridToRusr :: [Square] -> Maybe [Maybe String]
gridToRusr sqs = 
  if any isJust strs then Just strs else Nothing
  where
    strs :: [Maybe String]
    strs = map (\sq -> case sq of {Rebus s _ -> Just s; _ -> Nothing}) sqs

-- The first string is the board.  The second string is the rebus board
-- (or all 0s if none exists).  The [(Int,String)] is the rebus table,
-- (or an empty list of there aren't any to lookup).  The third String
-- is the extras board.
--
-- The bool should be True if this is a game board and false if it is a
-- solution board. 
--
-- gameOrSol  squares  grbs  rtbl  rtbl  gext  rusr
readBoard :: Bool -> Array Index CUChar -> Array Index CUChar 
          -> [(Int,String)] -> Array Index CUChar 
          -> Array Index (Maybe String)
          -> Array Index Square
readBoard isGame bd rbs rtbl ext rusr = 
    let convChar = charToSquare isGame rtbl in
      array (bounds bd) 
            (map (\(i,c) -> (i,convChar c (rbs ! i) (ext ! i) (rusr ! i)))
                 (assocs bd))

-- width, height.  List had better be the right length
listToBoard :: Int -> Int -> [a] -> Array Index a
listToBoard width height la =
  let -- these guys are in row-major order, so we need to flip
      numberFold :: (Int,Int,[(Index,a)]) -> a ->
                    (Int,Int,[(Index,a)])
      numberFold (x,y,l) sq = 
        let (x',y') = if x+1 == width then (0,y+1) else (x+1,y) in
          (x',y',(((x,y),sq):l))
  in
    array ((0,0),(width-1,height-1)) $ (\(_,_,l) -> l) $
          foldl' numberFold (0,0,[]) la

boardCharsOut :: Int -> Int -> Ptr CUChar -> IO (Array Index CUChar)
boardCharsOut width height ptr =
  do cuchars <- peekArray (width*height) ptr
     return $ listToBoard width height cuchars

numberClues :: [String] -> Array Index Square -> [(Int,Dir,String)]
numberClues cls bd =
  zipWith (\(a,b) c -> (a,b,c)) (findclues 1 (0,0)) cls
  where
    (_,(xmax,ymax)) = bounds bd

    -- sq number -> position -> list of places clues are needed
    findclues :: Int -> Index -> [(Int,Dir)]
    findclues n (x,y) =
        if black then rec else
          case (asq,bsq) of
            (True,True) -> (n,Across) : (n,Down) : rec
            (True,False) -> (n,Across) : rec
            (False,True) -> (n,Down) : rec
            (False,False) -> rec

      where
        black = bd ! (x,y) == Black
        asq = x == 0 || bd ! (x-1,y) == Black 
        bsq = y == 0 || bd ! (x,y-1) == Black

        nextind = if x == xmax then
                    if y == ymax then Nothing else Just (0,y+1)
                  else Just (x+1,y)

        nextnum = if (not black) && (asq || bsq) then n+1 else n

        rec = case nextind of Nothing -> []
                              Just ind -> findclues nextnum ind

-- fromRusr and toRusr go between the internal rusr format (a uchar **)
-- and the one more convenient for us (a [Maybe String])
fromRusr :: Int -> Ptr (Ptr CUChar) -> IO [Maybe String]
fromRusr len arr =
    do ptrs <- peekArray len arr
       mapM (\ptr -> if ptr == nullPtr then return Nothing
                        else do chrs <- peekArray0 0 ptr 
                                return $ Just $ map cucharToChar chrs)
            ptrs


-- These internal functions, fromPuz and toPuz, martial between
-- the internal Puz pointers and the exposed Puzzle datastructure

fromPuz :: Puz -> IO Puzzle
fromPuz puz = 
  do width  <- puzGetWidth puz
     height <- puzGetHeight puz
     let bdChrs   = boardCharsOut width height
         emptyBd  = listArray ((0,0),(width-1,height-1)) (repeat $ toEnum 0)
         bdSize   = width*height
         nothings = replicate bdSize Nothing

     -- Now get all the raw strings we need from the internal 
     -- puz structure
     gridChrs  <- puzGetGrid puz >>= bdChrs
     solChrs   <- puzGetSolution puz >>= bdChrs
     
     title     <- puzGetTitle puz
     author    <- puzGetAuthor puz
     copyright <- puzGetCopyright puz
     notes     <- puzGetNotes puz

     hasTimer  <- puzHasTimer puz
     timer     <- if hasTimer 
                    then liftM2 (\x y -> Just (x,y)) 
                           (puzGetTimerElapsed puz) 
                           (puzGetTimerStopped puz)
                    else return Nothing
     
     hasRebus  <- puzHasRebus puz
     rebusChrs <- if hasRebus then puzGetRebus puz >>= bdChrs
                              else return emptyBd
     rebusTbl  <- if hasRebus then puzGetRtbl puz else return []

     hasRusr   <- puzHasRusr puz
     rusrList  <- if hasRusr then puzGetRusr puz >>= fromRusr bdSize
                             else return nothings
     let rusrBoard = listToBoard width height rusrList
     
     hasExtras <- puzHasExtras puz
     extraChrs <- if hasExtras then puzGetExtras puz >>= bdChrs
                               else return emptyBd
     
     clueCount <- puzGetClueCount puz
     clueStrs  <- mapM (puzGetClue puz) [0..(clueCount-1)]

     isScrambled <- puzIsLockedGet puz
     locked <- if isScrambled then liftM Just $ puzLockedCksumGet puz
                              else return Nothing

     -- we use these strings and the puz data to get everything we
     -- need to build a Puzzle
     let grid, solution :: Array Index Square
         grid     = readBoard True gridChrs rebusChrs rebusTbl extraChrs
                              rusrBoard
         solution = readBoard False solChrs rebusChrs rebusTbl extraChrs
                              rusrBoard
     
         clues :: [(Int,Dir,String)]
         clues = numberClues clueStrs grid
     
     return $
       Puzzle {width, height, grid, solution, title, author, copyright,
               notes, timer, clues, locked}
    

toPuz :: Puzzle -> IO (Either ErrMsg Puz)
toPuz (Puzzle {width, height, grid, solution, title, author, notes, 
               copyright, timer, clues, locked}) =
  let clueCount = length clues
      clueStrs  = map (\(_,_,s) -> s) (sortBy orderClues clues)

      -- since these arrays are row-major but that's the wrong order for
      -- libpuz, we flip the indices first, which gets us a list in the
      -- right order
      gridSqs,solSqs :: [Square]
      gridSqs = elems (ixmap ((0,0),(height-1,width-1)) 
                             (\(a,b) -> (b,a)) grid)
      solSqs  = elems (ixmap ((0,0),(height-1,width-1)) 
                             (\(a,b) -> (b,a)) solution)

      userBoard,solBoard :: [CUChar]
      userBoard   = map squareToBoardChar gridSqs
      solBoard    = map squareToBoardChar solSqs

      extrasBoard :: Maybe [CUChar]
      extrasBoard = gridToExtras solSqs

      rebusInfo :: Maybe ([(String,Int)],[CUChar])
      rebusInfo = gridToRebus solSqs

      rusrInfo :: Maybe [Maybe String]
      rusrInfo = gridToRusr gridSqs
  in
  do puz <- puzCreate
            
     -- set the easy stuff that is marshalled by Internal
     puzSetWidth     puz width
     puzSetHeight    puz height

     puzSetTitle     puz title
     puzSetAuthor    puz author
     puzSetNotes     puz notes
     puzSetCopyright puz copyright

     case timer of
       Nothing -> return ()
       Just (e,s) -> puzSetTimer puz e s

     puzSetClueCount puz clueCount
     mapM_ (\(n,c) -> puzSetClue puz n c) (zip [0..] clueStrs)

     withArray0 0 userBoard (puzSetGrid puz)
     withArray0 0 solBoard (puzSetSolution puz)

     case extrasBoard of
       Nothing -> return ()
       Just b -> withArray0 0 b (puzSetExtras puz)
       
     case rebusInfo of
       Nothing -> return ()
       Just (rtbl,rbd) -> do withArray0 0 rbd (puzSetRebus puz)
                             puzSetRtbl puz rtbl
     case rusrInfo of
       Nothing -> return ()
       Just rusr -> puzSetRusr puz rusr

     case locked of
       Nothing -> return ()
       Just cksum -> puzLockSet puz cksum
                             
     puzCksumsCalc puz
     puzCksumsCommit puz
     cksumChk <- puzCksumsCheck puz

     return $
       if not cksumChk 
         then Left "Internal Error: Checksum calculation failed." 
         else Right puz

{- ---- Exposed library ---- -}

numberGrid :: Array Index Square -> Array Index (Maybe Int)
numberGrid grid = 
  array (bounds grid) bd_ass
  where
    indexCompare :: Index -> Index -> Ordering
    indexCompare (i1,i2) (j1,j2) = case compare i2 j2 of
                                     LT -> LT
                                     GT -> GT
                                     EQ -> compare i1 j1

    ass :: [(Index,Square)]
    ass = sortBy (\(i,_) (j,_) -> indexCompare i j) $ assocs grid

    isEmpty :: Index -> Bool
    isEmpty i = case lookup i ass of
                  Nothing           -> True
                  Just Black        -> True
                  Just (Letter _ _) -> False
                  Just (Rebus _ _)  -> False

    folder :: (Int, [(Index, Maybe Int)]) -> 
              (Index,Square) -> 
              (Int, [(Index, Maybe Int)])
    folder (ct,ns) (i@(ix,iy),sq) =
      let up_e, left_e :: Bool
          up_e   = isEmpty (ix,iy-1)
          left_e = isEmpty (ix-1,iy)
      in
      case sq of 
        Black -> (ct, (i,Nothing) : ns)
        Letter _ _ -> 
            if up_e || left_e 
              then (ct+1, (i, Just ct) : ns)
              else (ct  , (i, Nothing) : ns)
        Rebus _ _  -> 
            if up_e || left_e 
              then (ct+1, (i, Just ct) : ns)
              else (ct  , (i, Nothing) : ns)

    bd_ass :: [(Index, Maybe Int)]
    (_,bd_ass) = foldl folder (1,[]) ass

-- This is pretty inefficient!
unlockPuz :: Puzzle -> CUShort -> IO (Either ErrMsg Puzzle)
unlockPuz puzzle code =
  do epuz <- toPuz puzzle
     case epuz of
       Left err -> return $ Left err
       Right puz -> 
         do worked <- puzUnlockSolution puz code
            if worked
              then liftM Right $ fromPuz puz
              else return $ Left (   "Code " ++ show code 
                                  ++ " didn't unlock the puzzle.")

bruteForceUnlockPuz :: Puzzle -> IO (Either ErrMsg (Puzzle,Int))
bruteForceUnlockPuz puzzle =
  do epuz <- toPuz puzzle
     case epuz of
       Left err -> return $ Left err
       Right puz -> 
         do worked <- puzBruteForceUnlock puz
            case worked of
              Nothing -> return $ Left $    
                   "Sorry, no possible code successfully unlocked this "
                ++ "puzzle.  It may be ill-formed."
              Just code -> do puz' <- fromPuz puz
                              return $ Right (puz',code)

loadPuzzle :: String -> IO (Either Puzzle ErrMsg)
loadPuzzle fname =
  do --- Start by getting internal puz representation
     ehandle <- try (openFile fname ReadMode)
     case ehandle of
       Left err -> 
         if isDoesNotExistError err
           then return $ Right $ "File " ++ fname ++ " does not exist."
           else 
             if isPermissionError err
               then
                 return $ Right $ "Cannot access file " ++ fname ++ 
                                  ". (permissions error)"
               else return $ Right $ "Cannot open " ++ fname
       Right handle -> do 
         size <- liftM fromIntegral $ hFileSize handle
         bytestring <- hGetContents handle
         hClose handle
         
         let cchars :: [CUChar]
             cchars = foldr' (\w cs -> (fromIntegral w) : cs) [] bytestring
         mpuz <- withArray cchars (\ar -> puzLoad ar size)
         case mpuz of
           Nothing -> return $ Right "Ill-formed puzzle"
           Just puz -> 
            (puzCksumsCheck puz >>= 
               \v -> if not v 
                       then return $ Right "Ill-formed puzzle: bad checksums"
                       else liftM Left $ fromPuz puz)

savePuzzle :: String -> Puzzle -> IO (Maybe ErrMsg)
savePuzzle fname puzzle =
  do mpuz <- toPuz puzzle
     case mpuz of
       Left err -> return $ Just err
       Right puz ->
         do sz <- puzSize puz
            allocaArray sz
              (\ ptr -> 
                  do saveChk <- puzSave puz ptr sz
                     if not saveChk 
                       then return $ Just "Internal Error: puzSave failed."
                       else catch 
                              (do handle <- openFile fname WriteMode
                                  hPutBuf handle ptr sz
                                  hClose handle
                                  return Nothing)
                              (\err -> return $ Just $ show err))


stringCksum :: String -> IO CUShort
stringCksum s = puzCksumString s (length s)