--- /dev/null
+-- Writeup at https://work.njae.me.uk/2022/12/15/advent-of-code-2022-day-14/
+
+import AoC
+import Data.Text (Text)
+import qualified Data.Text.IO as TIO
+import Data.Attoparsec.Text hiding (take, D)
+import Control.Applicative
+import Data.List
+import Data.Ix
+import Data.Maybe
+import qualified Data.Set as S
+import Linear -- hiding (Trace, trace, distance)
+import Control.Lens
+
+type Position = V2 Int -- x, y, y increasing down.
+
+type Cave = S.Set Position
+
+data Sand = Falling Position | Blocked Position | Escaped
+ deriving (Eq, Show)
+
+-- open floor: sand escapes below given level
+-- closed floor: sand blocked by floor with this y
+data Floor = Open Int | Closed Int deriving (Eq, Show)
+
+main :: IO ()
+main =
+ do dataFileName <- getDataFileName
+ text <- TIO.readFile dataFileName
+ let corners = successfulParse text
+ let stone = mkCave corners
+ let floorY = fromJust $ maximumOf (folded . _y) stone
+ print $ part1 stone floorY
+ print $ part2 stone floorY
+
+part1, part2 :: Cave -> Int -> Int
+part1 stone floorY = sandQty
+ where filledCave = dropManySand stone (Open floorY)
+ sandQty = (S.size filledCave) - (S.size stone)
+part2 stone floorY = sandQty
+ where filledCave = dropManySand stone (Closed (floorY + 2))
+ sandQty = (S.size filledCave) - (S.size stone)
+
+mkCave :: [[Position]] -> Cave
+mkCave walls = foldl' addWall S.empty walls
+
+addWall :: Cave -> [Position] -> Cave
+addWall cave wall = foldl' addSegment cave segments
+ where segments = zip wall $ tail wall
+
+addSegment :: Cave -> (Position, Position) -> Cave
+addSegment cave segment = S.union cave segments
+ where segments = S.fromList $ range (uncurry min segment, uncurry max segment)
+
+fallDirections :: [Position]
+fallDirections = [V2 0 1, V2 -1 1, V2 1 1]
+
+sandOrigin :: Position
+sandOrigin = V2 500 0
+
+fallStep :: Sand -> Cave -> Floor -> Sand
+fallStep (Blocked here) _ _ = Blocked here
+fallStep Escaped _ _ = Escaped
+fallStep (Falling here) cave (Open floorY)
+ | here ^. _y > floorY = Escaped
+ | otherwise = maybe (Blocked here) Falling $ find vacant
+ $ fmap (here ^+^) fallDirections
+ where vacant there = there `S.notMember` cave
+fallStep (Falling here) cave (Closed floorY) =
+ maybe (Blocked here) Falling $ find vacant $ fmap (here ^+^) fallDirections
+ where vacant there = (there ^. _y < floorY) && (there `S.notMember` cave)
+
+fallsTo :: Sand -> Cave -> Floor -> Sand
+fallsTo here cave floorY =
+ case fallStep here cave floorY of
+ Escaped -> Escaped
+ Blocked there -> Blocked there
+ Falling there -> fallsTo (Falling there) cave floorY
+
+dropManySand :: Cave -> Floor -> Cave
+dropManySand cave floorY
+ | sandOrigin `S.member` cave = cave
+ | otherwise = case dropOneSand cave floorY of
+ Nothing -> cave
+ Just cave' -> dropManySand cave' floorY
+
+dropOneSand :: Cave -> Floor -> Maybe Cave
+dropOneSand cave floorY =
+ case (fallsTo (Falling sandOrigin) cave floorY) of
+ Escaped -> Nothing
+ Blocked there -> Just (S.insert there cave)
+ Falling _ -> error "sand still falling"
+
+-- Parse the input file
+
+wallsP :: Parser [[Position]]
+wallP :: Parser [Position]
+cornerP :: Parser Position
+
+wallsP = wallP `sepBy` endOfLine
+wallP = cornerP `sepBy` " -> "
+cornerP = V2 <$> (decimal <* ",") <*> decimal
+
+successfulParse :: Text -> [[Position]]
+successfulParse input =
+ case parseOnly wallsP input of
+ Left _err -> [] -- TIO.putStr $ T.pack $ parseErrorPretty err
+ Right corners -> corners