{- |
=Basic Elements

==Simple Boxes

>>> :{
aa2u "++  +-----+  +--+--+-----+  \n\
     \++  +--+  |  |  |  |     |  \n\
     \       |  |  +--+--+--+--+  \n\
     \+---+  |  |  |     |  |  |  \n\
     \+---+  +--+  +-----+--+--+  "
:}
┌┐  ┌─────┐  ┌──┬──┬─────┐
└┘  └──┐  │  │  │  │     │
       │  │  ├──┴──┼──┬──┤
┌───┐  │  │  │     │  │  │
└───┘  └──┘  └─────┴──┴──┘

==Rounded Boxes

>>> :{
aa2u "..  .-----.  .--+--+-----.  \n\
     \''  '--.  |  |  |  |     |  \n\
     \       |  |  +--+--+--+--+  \n\
     \.---.  |  |  |     |  |  |  \n\
     \'---'  '--'  '-----+--+--'  "
:}
╭╮  ╭─────╮  ╭──┬──┬─────╮
╰╯  ╰──╮  │  │  │  │     │
       │  │  ├──┴──┼──┬──┤
╭───╮  │  │  │     │  │  │
╰───╯  ╰──╯  ╰─────┴──┴──╯

==Dotted and double strokes

>>> :{
aa2u "++  .-----.  +==+==+=====+  \n\
     \++  +==+  :  |  :  |     |  \n\
     \       :  :  +==+==+==+==+  \n\
     \+===+  :  :  |     |  :  |  \n\
     \+---+  '--'  +=====+==+==+  "
:}
┌┐  ╭─────╮  ╒══╤══╤═════╕
└┘  ╘══╕  ┆  │  ┆  │     │
       ┆  ┆  ╞══╧══╪══╤══╡
╒═══╕  ┆  ┆  │     │  ┆  │
└───┘  ╰──╯  ╘═════╧══╧══╛

==Cast shadows

>>> :{
aa2u "+-------------+   \n\
     \|             |   \n\
     \+---+     +---+#  \n\
     \  ##|     |#####  \n\
     \    |     |#      \n\
     \    +-----+#      \n\
     \      ######      "
:}
┌─────────────┐
│             │
└───┐     ┌───┘█
  ██│     │█████
    │     │█
    └─────┘█
      ██████

=Properties

==Idempotent

Already rendered portions are not affected:

>>> :{
aa2u "┌┐  ╭─────╮  ╒══╤══╤═════╕  ┌───┐   \n\
     \└┘  ╘══╕  ┆  │  ┆  │     │  │   │   \n\
     \       ┆  ┆  ╞══╧══╪══╤══╡  │   │█  \n\
     \╒═══╕  ┆  ┆  │     │  ┆  │  └───┘█  \n\
     \└───┘  ╰──╯  ╘═════╧══╧══╛    ████  "
:}
┌┐  ╭─────╮  ╒══╤══╤═════╕  ┌───┐
└┘  ╘══╕  ┆  │  ┆  │     │  │   │
       ┆  ┆  ╞══╧══╪══╤══╡  │   │█
╒═══╕  ┆  ┆  │     │  ┆  │  └───┘█
└───┘  ╰──╯  ╘═════╧══╧══╛    ████

==Incremental

Existing characters can be removed:

>>> :{
aa2u "┌──┬ ┬──┐\n\
     \   │ │  │\n\
     \╞══╪ ╪══╡\n\
     \│        \n\
     \├──┼ ┼──┤\n\
     \   │ │  │\n\
     \└──┴ ┴──┘"
:}
───┐ ┌──┐
   │ │  │
╒══╛ ╘══╛
│
└──┐ ┌──┐
   │ │  │
───┘ └──┘

New connections can be added:

>>> :{
aa2u "┌──┐-┌──┐\n\
     \│  │ │  │\n\
     \╘══╛=╘══╛\n\
     \|  : :  |\n\
     \┌──┐-┌──┐\n\
     \│  │ │  │\n\
     \└──┘-└──┘"
:}
┌──┬─┬──┐
│  │ │  │
╞══╪═╪══╡
│  ┆ ┆  │
├──┼─┼──┤
│  │ │  │
└──┴─┴──┘

Existing connections can be altered by replacing/adding characters:

>>> :{
aa2u "┌──+──┐  .─────+─────.  ┌────┐    \n\
     \│  +==+  │     |     │  │####│    \n\
     \+==+  |  │     |     │  │#   │█   \n\
     \└──+─-┘  │     +-----+  └────┘█#  \n\
     \         +=====+     │    █████#  \n\
     \╭──+─-╮  │     |     │     #####  \n\
     \│  |  |  │     |     │            \n\
     \╰──+──╯  '───────────'            "
:}
┌──┬──┐  ╭─────┬─────╮  ┌────┐
│  ╞══╡  │     │     │  │████│
╞══╡  │  │     │     │  │█   │█
└──┴──┘  │     ├─────┤  └────┘██
         ╞═════╡     │    ██████
╭──┬──╮  │     │     │     █████
│  │  │  │     │     │
╰──┴──╯  ╰───────────╯

=Limitations

Some connections do not work as expected (mostly because the corresponding
Unicode characters do not exist), e.g. rounded corners with double-stroke lines,
or connection pieces connecting horizontal single- and double-stroke lines:

>>> :{
aa2u "--+==  .==.  .--.--.   \n\
     \  |    |  |  |  |  |   \n\
     \==+--  '=='  .--+--'   \n\
     \  |          |  |  |   \n\
     \--+==  --==  '--'--'   "
:}
──┐══  ╒══╕  ╭──╮──╮
  │    │  │  │  │  │
══├──  ╘══╛  ╰──┼──╯
  │          │  │  │
──┘══  ──══  ╰──╰──╯

-}
{-# LANGUAGE DeriveFunctor   #-}
{-# LANGUAGE LambdaCase      #-}
{-# LANGUAGE RecordWildCards #-}

module Text.AsciiArt where

import           Control.Comonad
import           Data.Maybe



--------------------------------------------------------------------------------
-- * Zipper
--------------------------------------------------------------------------------

-- | The 'Zipper' is assumed to be infinite, i.e. filled with empty values
-- outside the defined area.
data Zipper a = Zipper
    { before  :: [a]
    , current :: a
    , after   :: [a] }
    deriving (Functor)

moveBefore, moveAfter :: Zipper a -> Zipper a
moveBefore zipper@Zipper { before = a : as, current = b, after = cs }
    = zipper { before = as, current = a, after = b : cs }
moveAfter  zipper@Zipper { before = as, current = b, after = c : cs }
    = zipper { before = b : as, current = c, after = cs }

-- | Renders the 'current' and the @n - 1@ elements 'after' as list.
zipperToList :: Int -> Zipper a -> [a]
zipperToList n Zipper{..} = current : take (n - 1) after

-- | An infinite 'Zipper' filled with @a@s.
zipperOf :: a -> Zipper a
zipperOf a = Zipper { before = repeat a, current = a, after = repeat a }

-- | Takes a list and creates a 'Zipper' from it. The 'current' element will be
-- the 'head' of the list, and 'after' that 'tail'. The rest will be filled with
-- @a@s to an infinite 'Zipper'.
zipperFromList :: a -> [a] -> Zipper a
zipperFromList a = \case
    []     -> zipperOf a
    b : bs -> (zipperOf a) { current = b, after = bs ++ repeat a }


instance Comonad Zipper where
    extract = current
    extend f zipper = fmap f $ Zipper
        { before  = iterate1 moveBefore zipper
        , current = zipper
        , after   = iterate1 moveAfter zipper }
      where
        iterate1 f x = tail (iterate f x)



--------------------------------------------------------------------------------
-- * Plane (two-dimensional 'Zipper')
--------------------------------------------------------------------------------

-- | A plane is a 'Zipper' of 'Zipper's. The outer layer zips through lines
-- (up\/down), the inner layer through columns (left\/right).
-- Like the 'Zipper', the 'Plane' is assumed to be infinite in all directions.
newtype Plane a = Plane { unPlane :: Zipper (Zipper a) }
    deriving (Functor)

moveLeft, moveRight, moveUp, moveDown :: Plane a -> Plane a
moveLeft  = Plane . fmap moveBefore . unPlane
moveRight = Plane . fmap moveAfter  . unPlane
moveUp    = Plane . moveBefore      . unPlane
moveDown  = Plane . moveAfter       . unPlane


-- | Renders @m@ lines and @n@ columns as nested list.
planeToList :: Int -> Int -> Plane a -> [[a]]
planeToList m n (Plane Zipper{..}) = fmap (zipperToList n) $
    current : take (m - 1) after

-- | An infinite 'Plane' filled with @a@s.
planeOf :: a -> Plane a
planeOf a = Plane $ Zipper
    { before  = repeat (zipperOf a)
    , current = zipperOf a
    , after   = repeat (zipperOf a) }

-- | Create a 'Plane' from a list of lists, filling the rest with @a@s in all
-- directions.
planeFromList :: a -> [[a]] -> Plane a
planeFromList a = \case
    []       -> planeOf a
    as : ass -> Plane $ (zipperOf (zipperOf a))
        { current = zipperFromList a as
        , after = fmap (zipperFromList a) ass ++ repeat (zipperOf a) }


instance Comonad Plane where
    extract = current . current . unPlane
    extend f plane = fmap f $ Plane $ Zipper
        { before  = fmap foo (iterate1 moveUp plane)
        , current = foo plane
        , after   = fmap foo (iterate1 moveDown plane) }
      where
        foo p = Zipper
            { before = iterate1 moveLeft p
            , current = p
            , after = iterate1 moveRight p }
        iterate1 f x = tail (iterate f x)



--------------------------------------------------------------------------------
-- * Patterns
--------------------------------------------------------------------------------

newtype Pattern = Pattern ((Char, Char, Char), (Char, Char, Char), (Char, Char, Char))


patternFromString :: String -> Pattern
patternFromString [a, b, c, d, e, f, g, h, i] = Pattern ((a, b, c), (d, e, f), (g, h, i))
patternFromString _ = undefined

patternToString :: Pattern -> String
patternToString (Pattern ((a, b, c), (d, e, f), (g, h, i))) = [a, b, c, d, e, f, g, h, i]

-- | Find the 'Char' to replace the center of a 'Pattern'.
lookupPattern :: Pattern -> Maybe Char
lookupPattern pattern = case filter (satisfies pattern) patterns of
    []    -> Nothing
    a : _ -> Just (snd a)
  where
    satisfies :: Pattern -> (Pattern, Char) -> Bool
    satisfies diagram (pattern, _)
        = and (zipWith connectsLike (patternToString diagram) (patternToString pattern))

-- | Whether a character can connect to another character. For example, @+@
-- connects both horizontally (like @-@) and vertically (like @|@), so it
-- 'connectsLike' @-@, @|@, and of course like itself.
connectsLike :: Char -> Char -> Bool
char `connectsLike` pattern = case pattern of
    '-'   -> char `elem` ['-', '>', '<', '─'] || char `connectsLike` '+'
    '='   -> char `elem` ['=', '>', '<', '═'] || char `connectsLike` '+'
    '|'   -> char `elem` ['|', '^', 'v', '│'] || char `connectsLike` ':' || char `connectsLike` '+'
    ':'   -> char `elem` [':', '┆']
    '+'   -> char `elem` [ '+'
                         , '└', '┘', '┌', '┐'
                         , '╘', '╛', '╒', '╕'
                         , '├', '┤', '┬', '┴', '┼'
                         , '╞', '╡', '╤', '╧', '╪' ]
                         || char `connectsLike` '.'
    '.'   -> char `elem` [ '\'', '.'
                         , '╭', '╮', '╯', '╰' ]
    '\''  -> char `connectsLike` '.'
    ' '   -> True
    other -> char == other

-- | The actual pattern definitions. For convenience, the simple patterns are at
-- the top, and more complex ones at the bottom. 'lookupPattern' will first try
-- the most complex pattern and work its way to the simpler patterns, thus
-- avoiding to choose a simpler pattern and forgetting some connection.
patterns :: [(Pattern, Char)]
patterns = reverse $ fmap (\(a, b) -> (patternFromString a, b))
    [ ( "   \
        \ --\
        \   ", '─' )

    , ( "   \
        \-- \
        \   ", '─' )

    , ( "   \
        \ ==\
        \   ", '═' )

    , ( "   \
        \== \
        \   ", '═' )

    , ( "   \
        \ | \
        \ | ", '│' )

    , ( " | \
        \ | \
        \   ", '│' )

    , ( "   \
        \ : \
        \ | ", '┆' )

    , ( " | \
        \ : \
        \   ", '┆' )

    , ( "   \
        \ : \
        \ : ", '┆' )

    , ( " : \
        \ : \
        \   ", '┆' )

    , ( " | \
        \=+ \
        \   ", '╛' )

    , ( " | \
        \ +=\
        \   ", '╘' )

    , ( "   \
        \ +=\
        \ | ", '╒' )

    , ( "   \
        \=+ \
        \ | ", '╕' )

    , ( " | \
        \ +=\
        \ | ", '╞' )

    , ( " | \
        \=+ \
        \ | ", '╡' )

    , ( "   \
        \=+=\
        \ | ", '╤' )

    , ( " | \
        \=+=\
        \   ", '╧' )

    , ( " | \
        \=+=\
        \ | ", '╪' )

    , ( " | \
        \-+ \
        \   ", '┘' )

    , ( " | \
        \ +-\
        \   ", '└' )

    , ( "   \
        \ +-\
        \ | ", '┌' )

    , ( "   \
        \-+ \
        \ | ", '┐' )

    , ( " | \
        \ +-\
        \ | ", '├' )

    , ( " | \
        \-+ \
        \ | ", '┤' )

    , ( "   \
        \-+-\
        \ | ", '┬' )

    , ( " | \
        \-+-\
        \   ", '┴' )

    , ( " | \
        \-+-\
        \ | ", '┼' )

    , ( "   \
        \ .-\
        \ | ", '╭' )

    , ( "   \
        \-. \
        \ | ", '╮' )

    , ( " | \
        \-' \
        \   ", '╯' )

    , ( " | \
        \ '-\
        \   ", '╰' )

    , ( "   \
        \ # \
        \ # ", '█' )

    , ( "   \
        \## \
        \   ", '█' )

    , ( "   \
        \ ##\
        \   ", '█' )

    , ( " # \
        \ # \
        \   ", '█' )

    , ( "   \
        \-> \
        \   ", '▷' )

    , ( "   \
        \ <-\
        \   ", '◁' )

    , ( "   \
        \ ^ \
        \ | ", '△' )

    , ( " | \
        \ v \
        \   ", '▽' )
    ]



--------------------------------------------------------------------------------
-- * Transforming ASCII to Unicode
--------------------------------------------------------------------------------

-- | Match the 'current' element and its eight neighbours against the defined
-- 'patterns' and choose the 'Char' from the matching 'Pattern'.
substituteChar :: Plane Char -> Char
substituteChar = \case
    Plane ( Zipper ((Zipper (a : as) b (c : cs)) : _)
                   ( Zipper (d : ds) e (f : fs))
                   ((Zipper (g : gs) h (i : is)) : _) )
        -> fromMaybe e (lookupPattern (patternFromString [a, b, c, d, e, f, g, h, i]))
    _ -> undefined -- We assume an infinite Zipper!

-- | Transform a 'Plane' of ASCII characters to an equivalent plane where the
-- ASCII box drawings have been replaced by their Unicode counterpart.
--
-- This function is a convolution with 'substituteChar' using the 'Comonad'ic
-- 'extend'.
renderAsciiToUnicode :: Plane Char -> Plane Char
renderAsciiToUnicode = extend substituteChar



{- $setup
>>> :{
aa2u :: String -> IO ()
aa2u input =
    let inputLines = lines input
        plane = planeFromList ' ' inputLines
        width = maximum (fmap length inputLines)
        height = length inputLines
    in  putStr
        . unlines
        . fmap (reverse . dropWhile (== ' ') . reverse)
        . planeToList height width
        . renderAsciiToUnicode
        $ plane
:}
-}
