{-# LANGUAGE CPP #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia, TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DataKinds #-}

module Gen where

import System.FilePath
import Data.List.NonEmpty (NonEmpty(..))
import GHC.Generics
import Generic.Random
import Generics.Deriving.Show
import Prelude as P
import Test.Tasty.QuickCheck hiding ((==>))

import qualified Data.List.NonEmpty as NE


class AltShow a where
  altShow :: a -> String

instance {-# OVERLAPPABLE #-} Show a => AltShow a where
  altShow = show

instance {-# OVERLAPS #-} AltShow String where
  altShow = id

instance {-# OVERLAPPABLE #-} AltShow a => AltShow (Maybe a) where
  altShow Nothing = ""
  altShow (Just a) = altShow a


newtype WindowsFilePaths = WindowsFilePaths { unWindowsFilePaths :: [WindowsFilePath] }
  deriving (Show, Eq, Ord, Generic)

-- filepath = namespace *"\" namespace-tail
--          / UNC
--          / [ disk ] *"\" relative-path
--          / disk *"\"
data WindowsFilePath = NS NameSpace [Separator] NSTail
                     | UNC UNCShare
                     | N (Maybe Char) [Separator] (Maybe RelFilePath)
                     -- ^ This differs from the grammar, because we allow
                     -- empty paths
                     | PotentiallyInvalid FilePath
                     -- ^ this branch is added purely for the tests
  deriving (GShow, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[6, 2, 2, 1] `AndShrinking` WindowsFilePath)

instance Show WindowsFilePath where
  show wf = gshow wf ++ " (" ++ altShow wf ++ ")"

instance AltShow WindowsFilePath where
  altShow (NS ns seps nstail) = altShow ns ++ altShow seps ++ altShow nstail
  altShow (UNC unc) = altShow unc
  altShow (N mdisk seps mfrp) = maybe [] (:[]) mdisk ++ (altShow seps ++ altShow mfrp)
  altShow (PotentiallyInvalid fp) = fp


-- namespace-tail     = ( disk 1*"\" relative-path ; C:foo\bar is not valid
--                                                 ; namespaced paths are all absolute
--                      / disk *"\"
--                      / relative-path
--                      )
data NSTail = NST1 Char (NonEmpty Separator) RelFilePath
            | NST2 Char [Separator]
            | NST3 RelFilePath
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[1, 1, 1] `AndShrinking` NSTail)

instance AltShow NSTail where
  altShow (NST1 disk seps relfp) = disk:':':(altShow seps ++ altShow relfp)
  altShow (NST2 disk seps) = disk:':':altShow seps
  altShow (NST3 relfp) = altShow relfp


--  UNC = "\\" 1*pchar "\" 1*pchar  [ 1*"\" [ relative-path ] ]
data UNCShare = UNCShare Separator Separator
                         NonEmptyString
                         (NonEmpty Separator)
                         NonEmptyString
                         (Maybe (NonEmpty Separator, Maybe RelFilePath))
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[1] `AndShrinking` UNCShare)

instance AltShow UNCShare where
  altShow (UNCShare sep1 sep2 fp1 seps fp2 mrfp) = altShow sep1 ++ altShow sep2 ++ altShow fp1 ++ altShow seps ++ altShow fp2 ++ maybe "" (\(a, b) -> altShow a ++ maybe "" altShow b) mrfp

newtype NonEmptyString = NonEmptyString (NonEmpty Char)
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[1] `AndShrinking` NonEmptyString)

instance Semigroup NonEmptyString where
  (<>) (NonEmptyString ne) (NonEmptyString ne') = NonEmptyString (ne <> ne')

instance AltShow NonEmptyString where
  altShow (NonEmptyString ns) = NE.toList ns


-- | Windows API Namespaces
--
-- https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#namespaces
-- https://support.microsoft.com/en-us/topic/70b92942-a643-2f2d-2ac6-aad8acad49fb
-- https://superuser.com/a/1096784/854039
-- https://reverseengineering.stackexchange.com/a/15178
-- https://stackoverflow.com/a/25099634
--
-- namespace          = file-namespace / device-namespace / nt-namespace
-- file-namespace     = "\" "\" "?" "\"
-- device-namespace   = "\" "\" "." "\"
-- nt-namespace       = "\" "?" "?" "\"
data NameSpace = FileNameSpace
               | DeviceNameSpace
               | NTNameSpace
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[3, 1, 1] `AndShrinking` NameSpace)

instance AltShow NameSpace where
  altShow FileNameSpace = "\\\\?\\"
  altShow DeviceNameSpace = "\\\\.\\"
  altShow NTNameSpace = "\\??\\"


data Separator = UnixSep
               | WindowsSep
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[1, 1] `AndShrinking` Separator)

instance AltShow Separator where
  altShow UnixSep = "/"
  altShow WindowsSep = "\\"

instance {-# OVERLAPS #-} AltShow (NonEmpty Separator) where
  altShow ne = mconcat $ NE.toList (altShow <$> ne)

instance {-# OVERLAPS #-} AltShow [Separator] where
  altShow [] = ""
  altShow ne = altShow (NE.fromList ne)

--  relative-path = 1*(path-name 1*"\") [ file-name ] / file-name
data RelFilePath = Rel1 (NonEmpty (NonEmptyString, NonEmpty Separator)) (Maybe FileName)
                 | Rel2 FileName
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[2, 1] `AndShrinking` RelFilePath)

instance AltShow RelFilePath where
  altShow (Rel1 ns mf) = mconcat (NE.toList $ fmap (\(a, b) -> altShow a ++ altShow b) ns) ++ altShow mf
  altShow (Rel2 fn) = altShow fn

--  file-name = 1*pchar [ stream ]
data FileName = FileName NonEmptyString (Maybe DataStream)
  deriving (GShow, Show, Eq, Ord, Generic)

instance Arbitrary FileName where
  -- make sure that half of the filenames include a dot '.'
  -- so that we can deal with extensions
  arbitrary = do
    ns <- arbitrary
    ds <- arbitrary
    i <- chooseInt (0, 100)
    if i >= 50
    then do
           ns' <- arbitrary
           pure $ FileName (ns <> NonEmptyString ('.':|[]) <> ns') ds
    else pure $ FileName ns ds
  shrink = genericShrink


instance Arbitrary (Maybe DataStream) where
  arbitrary = genericArbitraryRec (1 % 1 % ())
  shrink = genericShrink

instance AltShow FileName where
  altShow (FileName ns ds) = altShow ns ++ altShow ds

--  stream = ":" 1*schar [ ":" 1*schar ] / ":" ":" 1*schar
data DataStream = DS1 NonEmptyString (Maybe NonEmptyString)
                | DS2 NonEmptyString -- ::datatype
  deriving (GShow, Show, Eq, Ord, Generic)
  deriving Arbitrary via (GenericArbitraryRec '[1, 1] `AndShrinking` DataStream)

instance AltShow DataStream where
  altShow (DS1 ns Nothing) = ":" ++ altShow ns
  altShow (DS1 ns (Just ns2)) = ":" ++ altShow ns ++ ":" ++ altShow ns2
  altShow (DS2 ns) = "::" ++ altShow ns

instance Arbitrary WindowsFilePaths where
  arbitrary = WindowsFilePaths <$> listOf' arbitrary
  shrink = genericShrink

instance Arbitrary [Separator] where
  arbitrary = listOf' arbitrary
  shrink = genericShrink

#if !MIN_VERSION_QuickCheck(2,17,0)
instance Arbitrary a => Arbitrary (NonEmpty a) where
  arbitrary = NE.fromList <$> listOf1' arbitrary
  shrink = genericShrink
#endif
