{-# LANGUAGE LambdaCase #-}
module Lab12 where

import           Control.Applicative
import           Data.Char
import           Data.Functor                   ( ($>) )
import           Data.Maybe                     ( fromMaybe )

import           Prelude                 hiding ( map )

data Block = Wall | Free | Star deriving (Eq, Show)

newtype Maze = M { getMaze :: [[Block]] }

maze :: Maze
maze = M
    [ [Wall, Wall, Wall, Wall, Wall]
    , [Wall, Free, Wall, Free, Wall]
    , [Wall, Free, Wall, Wall, Wall]
    , [Wall, Free, Free, Free, Wall]
    , [Wall, Wall, Wall, Wall, Wall]
    ]

instance Show Maze where
    show = unlines . fmap (fmap fmtCell) . getMaze
      where
        fmtCell :: Block -> Char
        fmtCell Wall = '#'
        fmtCell Free = ' '
        fmtCell Star = '*'


-- | Position in the maze
type Pos = (Int, Int)

-- | Path represented as a sequence of points
type Path = [Pos]

-- | Start, Goal, in Maze
type Task = (Pos, Pos, Maze)

safePut :: Int -> a -> [a] -> Maybe [a]
safePut n x xs
    | 0 <= n && n < length xs = Just $ take n xs ++ (x : drop (n + 1) xs)
    | otherwise               = Nothing

safeGet :: Int -> [a] -> Maybe a
safeGet n xs | 0 <= n && n < length xs = Just $ xs !! n
             | otherwise               = Nothing

getBlock :: Pos -> Maze -> Maybe Block
getBlock (x, y) (M m) = do
    row <- safeGet y m
    safeGet x row

setBlock :: Block -> Pos -> Maze -> Maybe Maze
setBlock b (x, y) (M m) = do
    row     <- safeGet y m
    newRow  <- safePut x b row
    newMaze <- safePut y newRow m
    pure (M newMaze)

setPath :: Block -> Path -> Maze -> Maybe Maze
setPath _ []            m = Just m
setPath b (pos : path') m = do
    m' <- setBlock b pos m
    setPath b path' m'

drawSolution :: Path -> Maze -> Maze
drawSolution solution m = fromMaybe m (setPath Star solution m)

neighbors :: Pos -> [Pos]
neighbors (x, y) =
    [ (x + dx, y + dy)
    | dx <- [-1 .. 1]
    , dy <- [-1 .. 1]
    , not (dx == 0 && dy == 0)
    ]

nextPosition :: Pos -> Maze -> [Pos]
nextPosition p m = [ n | n <- neighbors p, getBlock n m == Just Free ]

extend :: Path -> Maze -> [Path]
extend []           _ = []
extend path@(p : _) m = (: path) <$> nextPosition p m

bfs
    :: [Pos] -- ^ Visited positions
    -> [Path] -- ^ Path queue
    -> Pos -- ^ Objective
    -> Maze
    -> Maybe Path
bfs _ [] _ _ = Nothing
bfs visited (path@(p : _) : paths) target m
    | p == target      = Just $ reverse path
    | p `elem` visited = bfs visited paths target m
    | otherwise        = bfs (p : visited) (paths ++ extend path m) target m

solve :: Task -> Maybe Path
solve (start, target, m) = bfs [] [[start]] target m

newtype Parser a = P { parse :: String -> Maybe (a, String) }

instance Functor Parser where
    -- fmap :: (a -> b) -> Parser a -> Parser b
    fmap f p = P
        (\inp -> case parse p inp of
            Nothing       -> Nothing
            Just (v, out) -> Just (f v, out)
        )

instance Applicative Parser where
    -- (<*>) :: Parser (a -> b) -> Parser a -> Parser b
    pg <*> px = P
        (\inp -> case parse pg inp of
            Nothing       -> Nothing
            Just (g, out) -> parse (fmap g px) out
        )
    pure v = P (\inp -> Just (v, inp))

instance Monad Parser where
    -- (>>=) :: Parser a -> (a -> Parser b) -> Parser b
    p >>= f = P
        (\inp -> case parse p inp of
            Nothing       -> Nothing
            Just (v, out) -> parse (f v) out
        )

instance Alternative Parser where
    -- empty :: Parser a
    empty = P (const Nothing)
    -- (<|>) :: Parser a -> Parser a -> Parser a
    p <|> q = P
        (\inp -> case parse p inp of
            Nothing       -> parse q inp
            Just (v, out) -> Just (v, out)
        )

item :: Parser Char
item = P
    (\case
        ""       -> Nothing
        (x : xs) -> Just (x, xs)
    )

sat :: (Char -> Bool) -> Parser Char
sat pred = item >>= (\c -> if pred c then pure c else empty)

digit :: Parser Char
digit = sat isDigit

char :: Char -> Parser Char
char c = sat (== c)

string :: String -> Parser String
string ""         = pure ""
string s@(c : cs) = (char c *> string cs) $> s

space :: Parser ()
space = many (sat isSpace) $> ()

token :: Parser a -> Parser a
token p = space *> p <* space

pos :: Parser Pos
pos =
    char '('
        *> (   (\strX strY -> (read strX, read strY))
           <$> (token (some digit))
           <*> (char ',' *> token (some digit))
           )
        <* char ')'

block :: Parser Block
block = item >>= \case
    '#' -> pure Wall
    ' ' -> pure Free
    _   -> empty

row :: Parser [Block]
row = many block <* char '\n'

map :: Parser Maze
map = M <$> many row

start :: Parser Pos
start = string "start" *> token (char '=') *> pos <* char '\n'

goal :: Parser Pos
goal = string "goal" *> token (char '=') *> pos <* char '\n'

file :: Parser Task
file = (,,) <$> start <*> goal <*> map

solveTask :: Task -> String
solveTask t@(_, _, m) = case solve t of
    Nothing   -> "No solution exists"
    Just path -> show $ drawSolution path m

processInput :: String -> String
processInput input = case parse file input of
    Nothing        -> "File has syntax error"
    Just (t, ""  ) -> solveTask t
    Just (_, rest) -> "Unused input: " ++ rest

main :: IO ()
main = interact processInput
