-- For Dataable instances
{-# LANGUAGE InstanceSigs #-}
-- For Output
{-# LANGUAGE RankNTypes #-}
-- For Dataable instances
{-# LANGUAGE UndecidableInstances #-}

module Literate.Web.Semantics.Compiler where

import Control.Applicative (Applicative (..))
import Control.Monad (Monad (..), forM_)
import Control.Monad.Classes qualified as MC
import Data.Bool
import Data.ByteString.Builder qualified as BSB
import Data.Either (Either (..))
import Data.Eq (Eq)
import Data.Foldable (toList)
import Data.Function (id, ($), (.))
import Data.Functor (Functor (..), (<$>))
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Maybe (Maybe (..))
import Data.Monoid (Monoid (..))
import Data.Ord (Ord)
import Data.Proxy (Proxy (..))
import Data.Semigroup (Semigroup (..))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Tuple (fst, snd)
import GHC.Generics (Generic)
import GHC.Stack (HasCallStack)
import Symantic qualified as Sym
import System.Directory qualified as Sys
import System.FilePath qualified as Sys
import System.FilePattern.Directory qualified as Sys
import System.IO qualified as Sys
import Text.Show (Show (..))
import Type.Reflection ((:~:) (..))

import Literate.Web.Syntaxes
import Literate.Web.Types.MIME
import Literate.Web.Types.URL

-- * Type 'Compiler'

-- | Interpreter building a static Web site.
--
-- Embed a 'Monad' @m@ to give access to a model if need be.
newtype Compiler m a = Compiler {unCompiler :: m [Output a]}

data CompilerEndpoint m a next = CompilerEndpoint
  { compilerEndpointProof :: next :~: m BSB.Builder
  , compilerEndpointData :: a
  }
type instance Sym.ToFEndpoint (Compiler m) a next = CompilerEndpoint m a next

compilerEndpoint :: a -> CompilerEndpoint m a (m BSB.Builder)
compilerEndpoint = CompilerEndpoint Refl

compiler :: Compiler m (m BSB.Builder) -> Compiler m (m BSB.Builder)
compiler = id

compile ::
  MC.MonadExec Sys.IO m =>
  CompilerEnv ->
  Compiler m a ->
  (a --> m BSB.Builder) ->
  m ()
compile CompilerEnv{..} router content = do
  outputs <- unCompiler router
  MC.exec @Sys.IO $ Sys.removePathForcibly compilerEnvDest
  forM_ outputs $ \out -> do
    let outFullPath = outputPathFile (outputPathRelative out)
    -- MC.exec $ Sys.hPutStrLn Sys.stderr $ "mkdir: " <> show (Sys.takeDirectory outFullPath)
    MC.exec @Sys.IO $
      Sys.createDirectoryIfMissing True $
        compilerEnvDest Sys.</> Sys.takeDirectory outFullPath
    -- MC.exec $ Sys.hPutStrLn Sys.stderr $ "write: " <> show outFullPath
    bsb <- outputData out content
    MC.exec @Sys.IO do
      Sys.withBinaryFile (compilerEnvDest Sys.</> outFullPath) Sys.WriteMode $ \h -> do
        BSB.hPutBuilder h bsb

siteManifest :: Monad m => Compiler m a -> m [Sys.FilePath]
siteManifest router = (outputPathFile . outputPathRelative <$>) <$> unCompiler router

siteMap ::
  Monad m =>
  Compiler m a ->
  (a --> m BSB.Builder) ->
  m (Map.Map OutputPath (m BSB.Builder))
siteMap router content = do
  outputs <- unCompiler router
  return $
    Map.fromList
      [ (outputPathRelative out, outputData out content)
      | out <- outputs
      ]

-- ** Type 'CompilerEnv'
data CompilerEnv = CompilerEnv
  { compilerEnvDest :: Sys.FilePath
  , -- , compilerEnvSource :: Sys.FilePath
    compilerEnvIndex :: Sys.FilePath
    -- , compilerEnvModel :: model
    -- , compilerEnvPath :: [PathSegment]
  }
  deriving (Show)

-- instance Applicative m => Applicative (Compiler m) where
--   pure = Compiler . pure . pure . pure
--   liftA2 f (Compiler a) (Compiler b) = Compiler (liftA2 (liftA2 (liftA2 f)) a b)
-- instance Monad m => Monad (Compiler m) where
--   return = pure
--   Compiler mloa >>= a2mlob =
--     Compiler $ do
--       mloa >>= \loa ->
--         (join <$>) $
--           forM loa $ \oa -> do
--             lob <- unCompiler $ a2mlob $ outputData oa
--             forM lob $ \ob ->
--               return
--                 ob
--                   { outputPath = outputPath oa <> outputPath ob
--                   , outputExts = outputExts oa <> outputExts ob
--                   }
instance Applicative m => Sym.ProductFunctor (Compiler m) where
  Compiler a <.> Compiler b = Compiler $ liftA2 (liftA2 (<.>)) a b
  Compiler a <. Compiler b = Compiler $ liftA2 (liftA2 (<.)) a b
  Compiler a .> Compiler b = Compiler $ liftA2 (liftA2 (.>)) a b

-- instance Applicative m => Sym.AlternativeFunctor (Compiler m) where
--  Compiler a <|> Compiler b = Compiler (liftA2 (<>) a b)

instance Applicative m => Sym.SumFunctor (Compiler m) where
  Compiler a <+> Compiler b = Compiler $ liftA2 (\as bs -> (a2e <$> as) <> (b2e <$> bs)) a b
    where
      a2e :: Output a -> Output (Either a b)
      a2e o = o{outputData = outputData o . fst}
      b2e :: Output b -> Output (Either a b)
      b2e o = o{outputData = outputData o . snd}

instance (Applicative m, Sym.ToFable a) => Optionable a (Compiler m) where
  optional (Compiler ma) = Compiler $ (\as -> forNothing : (forJust <$> as)) <$> ma
    where
      forNothing :: Output (Maybe a)
      forNothing = Output{outputPath = mempty, outputData = ($ Nothing)}
      forJust :: Output a -> Output (Maybe a)
      forJust o = o{outputData = \k -> outputData o $ Sym.tofOffun $ k . Just}

instance Functor m => Sym.Voidable (Compiler m) where
  void _a (Compiler ma) =
    Compiler $
      (\os -> (\o -> o{outputData = id}) <$> os) <$> ma

--   optional (Compiler ma) = Compiler $ (\as -> forNothing : (forJust <$> as)) <$> ma
--     --pure Nothing Sym.<|> (Just <$> ma)
--     where
--     forNothing :: Output a -> Output (Maybe a)
--     forNothing o = o{ outputData = fst }
--     forJust :: Output a -> Output (Maybe a)
--     forJust o = o{ outputData = outputData o . snd }

instance
  ( Generic a
  , Sym.RepOfEoT a
  , sem ~ Compiler m
  , Sym.IsToF a ~ 'False
  , eot ~ Sym.EoT (Sym.ADT a)
  , Sym.ToFable eot
  , Functor m
  ) =>
  Dataable a (Compiler m)
  where
  dataType :: sem (Sym.EoT (Sym.ADT a)) -> sem a
  dataType (Compiler mos) = Compiler ((dataType <$>) <$> mos)
instance
  ( Generic a
  , Sym.RepOfEoT a
  , sem ~ Output
  , Sym.IsToF a ~ 'False
  , eot ~ Sym.EoT (Sym.ADT a)
  , Sym.ToFable eot
  ) =>
  Dataable a Output
  where
  dataType :: sem (Sym.EoT (Sym.ADT a)) -> sem a
  dataType o = o{outputData = \k -> (outputData o) $ Sym.tofOffun $ k . Sym.adtOfeot}
instance Applicative m => PathSegmentable (Compiler m) where
  pathSegment s =
    Compiler $
      pure
        [ Output
            { outputPath =
                OutputPath
                  { outputPathSegs = [s]
                  , outputPathExts = []
                  }
            , outputData = id
            }
        ]
instance
  ( Applicative m
  , n ~ m
  , MimeTypes ts (MimeEncodable a)
  ) =>
  Responsable a ts n (Compiler m)
  where
  response =
    Compiler $
      pure $
        ( \(_mediaType, MimeType (Proxy :: Proxy t)) ->
            Output
              { outputPath =
                  OutputPath
                    { outputPathSegs = []
                    , outputPathExts = [decodePathSegment (fileExtension @t)]
                    }
              , outputData = \(CompilerEndpoint Refl (Response ma)) ->
                  mimeEncode @_ @t <$> ma
              }
        )
          <$> toList (mimeTypesMap @ts @(MimeEncodable a))

-- ** Type 'Output'

data Output a = Output
  { outputPath :: OutputPath
  , outputData :: forall next. (a --> next) -> next
  }

outputBuilder :: Output a -> (a --> m BSB.Builder) -> m BSB.Builder
outputBuilder = outputData

outputPathRelative :: Output a -> OutputPath
outputPathRelative out
  | List.null (outputPathExts outPath) = outPath{outputPathExts = ["txt"]}
  | otherwise = outPath
  where
    outPath = outputPath out

-- *** Type 'OutputPath'
data OutputPath = OutputPath
  { outputPathSegs :: [PathSegment]
  , outputPathExts :: [PathSegment]
  }
  deriving (Eq, Ord, Show)

outputPathFile :: OutputPath -> Sys.FilePath
outputPathFile outPath =
  List.intercalate "." $
    encodePath (outputPathSegs outPath)
      : (Text.unpack . encodePathSegment <$> outputPathExts outPath)

instance Semigroup OutputPath where
  x <> y =
    OutputPath
      { outputPathSegs = outputPathSegs x <> outputPathSegs y
      , outputPathExts = outputPathExts x <> outputPathExts y
      }
instance Monoid OutputPath where
  mempty = OutputPath{outputPathSegs = [], outputPathExts = []}

instance Sym.ProductFunctor Output where
  a <.> b =
    Output
      { outputPath = outputPath a <> outputPath b
      , outputData = outputData b . outputData a
      }
  a <. b =
    Output
      { outputPath = outputPath a <> outputPath b
      , outputData = outputData b . outputData a
      }
  a .> b =
    Output
      { outputPath = outputPath a <> outputPath b
      , outputData = outputData b . outputData a
      }

-- compile :: Show a => Renderable a => Compiler a -> CompilerEnv -> Sys.IO ()
-- compile compiler conf@CompilerEnv{..} = do
--   createDirectoryIfMissing True compilerEnvDest
--   let router = unCompiler compiler
--   when (null router) $
--     error "no router, nothing to compile"
--   -- Sys.hPutStrLn Sys.stderr $ "conf: " <> show conf
--   -- forM_ router $ \comp -> do
--   --   Sys.hPutStrLn Sys.stderr $ "route: " <> show comp
--   forM_ router $ \Comp{..} -> do
--     -- Sys.hPutStrLn Sys.stderr $ "route: " <> show comp
--     let routePath = pathOfPathSegments compPathSegments
--      in case render compData of
--           Left staticPath ->
--             doesPathExist (compilerEnvSource </> staticPath) >>= \case
--               True -> do
--                 -- TODO: In current branch, we don't expect this to be a directory.
--                 -- Although the user may pass it, but review before merge.
--                 Sys.hPutStrLn Sys.stderr $
--                   "staticCopy: "
--                     <> show
--                       ( (compilerEnvSource </> staticPath)
--                       , (compilerEnvDest </> staticPath)
--                       )
--                 copyDirRecursively
--                   staticPath
--                   (compilerEnvSource </> staticPath)
--                   (compilerEnvDest)
--               False -> do
--                 Sys.hPutStrLn Sys.stderr $ "error: path does not exist: " <> (compilerEnvSource </> staticPath)
--           -- log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
--           Right bs -> do
--             let outputFullPath = compilerEnvDest </> routePath
--                            {-
--                            maybe (routePath </> compilerEnvIndex)
--                                  (routePath Sys.FilePath.<.>)
--                                  (compType comp)
--                           -}
--             -- Sys.hPutStrLn Sys.stderr $ "mkdir: " <> show (Sys.takeDirectory outputFullPath)
--             createDirectoryIfMissing True (Sys.takeDirectory outputFullPath)
--             -- Sys.hPutStrLn Sys.stderr $ "write: " <> show outputFullPath
--             BSL.writeFile outputFullPath bs

copyDirRecursively ::
  HasCallStack =>
  -- | Source file path relative to CWD
  Sys.FilePath ->
  -- | Absolute path to source file to copy.
  Sys.FilePath ->
  -- | Directory *under* which the source file/dir will be copied
  Sys.FilePath ->
  Sys.IO ()
copyDirRecursively srcRel srcAbs destParent = do
  Sys.doesFileExist srcAbs >>= \case
    True -> do
      Sys.hPutStrLn Sys.stderr $ "copyDirRecursively: " <> show ("srcAbs is a file" :: Text, srcAbs)
      copyFileCreatingParents srcAbs (destParent Sys.</> srcRel)
    False ->
      Sys.doesDirectoryExist srcAbs >>= \case
        False -> do
          Sys.hPutStrLn Sys.stderr $ "copyDirRecursively: " <> show ("srcAbs is neither a file nor directory" :: Text, srcAbs)
          return ()
        -- throw $ StaticAssetMissing srcAbs
        True -> do
          Sys.hPutStrLn Sys.stderr $ "copyDirRecursively: " <> show ("srcAbs is a directory" :: Text, srcAbs)
          Sys.createDirectoryIfMissing True (destParent Sys.</> srcRel)
          fs <- Sys.getDirectoryFiles srcAbs ["**"]
          forM_ fs $ \fp -> do
            let
              a = srcAbs Sys.</> fp
              b = destParent Sys.</> srcRel Sys.</> fp
            copyFileCreatingParents a b
  where
    copyFileCreatingParents a b = do
      Sys.createDirectoryIfMissing True (Sys.takeDirectory b)
      Sys.copyFile a b