{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Mismi.S3.Core.Data (
    WriteMode (..)
  , SyncMode (..)
  , Bucket (..)
  , Address (..)
  , Key (..)
  , ReadGrant (..)
  , WriteResult (..)
  , Bytes (..)
  , Sized (..)
  , (//)
  , combineKey
  , dirname
  , foldWriteMode
  , foldSyncMode
  , basename
  , addressFromText
  , addressToText
  , removeCommonPrefix
  , withKey
  , s3Parser
  ) where

import           Data.Attoparsec.Text (Parser)
import           Data.Attoparsec.Text (anyChar, char, manyTill, string, takeWhile)
import           Data.Attoparsec.Text (endOfInput, parseOnly)
import           Data.Data (Data, Typeable)
import           Data.List (drop, init, reverse, zipWith)
import           Data.String (String)
import qualified Data.Text as T

import           P
import           Prelude (Integral)

data WriteResult =
    WriteOk
  | WriteDestinationExists !Address
    deriving (Eq, Show)

-- |
-- Describes the semantics for destructive operation that may result in overwritten files.
--
data WriteMode =
    Fail        -- ^ Fail rather than overwrite any data.
  | Overwrite   -- ^ Overwrite existing data silently, i.e. we really want to do this.
    deriving (Eq, Show)

foldWriteMode :: a -> a -> WriteMode -> a
foldWriteMode f o m =
  case m of
    Fail ->
      f
    Overwrite ->
      o

data SyncMode =
    FailSync
  | OverwriteSync
  | SkipSync
    deriving (Eq, Show)

foldSyncMode :: a -> a -> a -> SyncMode -> a
foldSyncMode f o s m =
  case m of
    FailSync ->
      f
    OverwriteSync ->
      o
    SkipSync ->
      s

newtype Bucket =
  Bucket {
      unBucket :: Text
    } deriving (Eq, Show, Ord, Data, Typeable)

newtype Key =
  Key {
      unKey :: Text
    } deriving (Eq, Show, Ord, Data, Typeable)

data Address =
  Address {
      bucket :: !Bucket
    , key :: !Key
    } deriving (Eq, Show, Ord, Data, Typeable)

newtype ReadGrant =
  ReadGrant {
      readGrant :: Text
    } deriving (Eq, Show)

newtype Bytes =
  Bytes {
      unBytes :: Int64
    } deriving (Eq, Show, Ord, Enum, Num, Real, Integral)

data Sized a =
  Sized {
      sizedBytes :: !Bytes
    , sizedValue :: !a
    } deriving (Eq, Show, Ord, Functor, Foldable, Traversable)

(//) :: Key -> Key -> Key
(//) =
  combineKey

combineKey :: Key -> Key -> Key
combineKey (Key p1) (Key p2) =
  if  "/" `T.isSuffixOf` p1 || p1 == "" || "/" `T.isPrefixOf` p2 then
    Key $ p1 <> p2
  else
    Key $ p1 <> "/" <> p2

-- | @withKey f address@ : Replace the 'Key' part of an 'Address' with a new
--   'Key' resulting from the application of function @f@ to the old 'Key'.
withKey :: (Key -> Key) -> Address -> Address
withKey f (Address b k) =
  Address b $ f k

-- | Get the prefix for a given key (eg. dirname "\/foo\/bar" == "foo").
dirname :: Key -> Key
dirname =
  Key . T.intercalate "/" . init . T.split (=='/') . unKey

-- | Get the basename for a given key (eg. basename "\/foo\/bar" == "bar").
--   Return 'Nothing' for the empty 'Key' _and_ when the name ends with a "/".
basename :: Key -> Maybe Text
basename =
  mfilter (not . T.null) . listToMaybe . reverse . T.split (== '/') . unKey

-- prefix key
removeCommonPrefix :: Address -> Address -> Maybe Key
removeCommonPrefix prefix addr =
  let
    dropMaybe :: String -> String -> Maybe Text
    dropMaybe x y =
      bool
        Nothing
        (Just . T.pack $ drop (length y) x)
        (check x y)

    check :: String -> String -> Bool
    check x y =
      y == zipWith const x y
  in
  if bucket addr == bucket prefix then
     if unKey (key prefix) == "" then
        Just $ key addr
     else
       let
         bk = unKey (key prefix)
         b = bool (bk <> "/") bk ("/" `T.isSuffixOf` bk)
         pk = T.unpack b
         kk = T.unpack (unKey $ key addr)
       in
         Key <$> dropMaybe kk pk
  else
    Nothing

-- | Render an 'Address' to 'Text', including the "s3://" prefix.
addressToText :: Address -> Text
addressToText a =
  "s3://" <> unBucket (bucket a) <> "/" <> unKey (key a)

-- | Parse an 'Address' from 'Text'. If the parse fails, 'Nothing' is returned.
addressFromText :: Text -> Maybe Address
addressFromText =
  hush . parseOnly s3Parser

s3Parser :: Parser Address
s3Parser =
  s3Parser' <|> s3Parser''

s3Parser' :: Parser Address
s3Parser' = do
  _ <- string "s3://"
  b <- manyTill anyChar (char '/')
  k <- many anyChar
  pure $ Address (Bucket . T.pack $ b) (Key . T.pack $ k)

s3Parser'' :: Parser Address
s3Parser'' = do
  _ <- string "s3://"
  b <- takeWhile (/= '/')
  endOfInput
  pure $ Address (Bucket b) (Key "")