{-# LANGUAGE CPP #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Main where

import Control.Applicative ((<*>), pure)
import Control.Arrow ((&&&))
import Control.Monad (forM_)
import qualified Data.Aeson as A
import qualified Data.Aeson.TH as A
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.JSON.Schema.Generator as G
import qualified Data.List as List
import Data.Map (fromList)
import Data.Monoid ((<>))
import Data.Proxy (Proxy(Proxy))
import Data.Typeable (typeOf)
import GHC.Generics
import System.Exit (ExitCode(ExitSuccess), exitWith)
import System.IO (Handle, IOMode(WriteMode), hClose, hPutStr, hPutStrLn, withFile)
import System.Process (CreateProcess(..), StdStream(CreatePipe)
    , createProcess, proc, system, waitForProcess)

import Types
import Values

--
-- Instances
--

instance G.JSONSchemaGen RecordType1
instance G.JSONSchemaGen RecordType2
instance G.JSONSchemaGen ProductType1
instance G.JSONSchemaGen ProductType2
instance G.JSONSchemaGen UnitType1
instance G.JSONSchemaGen UnitType2
instance G.JSONSchemaGen MixType1

--instance G.JSONSchemaPrim UnitType2 where
--    toSchemaPrim opts _ = G.scSchemaType . G.toSchema opts $ (Proxy :: Proxy UnitType2)

--
-- TestData
--

data TestDatum =
#if MIN_VERSION_aeson(1,0,0)
    forall a. (Generic a, A.GToJSON A.Zero (Rep a), SchemaName (Rep a))
#else
    forall a. (Generic a, A.GToJSON (Rep a), SchemaName (Rep a))
#endif
        => TestDatum { tdName :: String
                     , tdValue :: a
                     }

#if MIN_VERSION_aeson(1,0,0)
testDatum :: (Generic a, A.GToJSON A.Zero (Rep a), SchemaName (Rep a)) => String -> a -> TestDatum
#else
testDatum :: (Generic a, A.GToJSON (Rep a), SchemaName (Rep a)) => String -> a -> TestDatum
#endif
testDatum = TestDatum

testData :: [TestDatum]
testData =
    [ TestDatum "recordData11"  recordType11
    , testDatum "recordData12"  recordType22
    , testDatum "productData11" productData11
    , testDatum "productData12" productData12
    , testDatum "unitData1"     unitData1
    , testDatum "unitData2"     unitData2
    , testDatum "unitData3"     unitData3
    , testDatum "mixData11"     mixData11
    , testDatum "mixData12"     mixData12
    , testDatum "mixData13"     mixData13
    ]

--
-- Encoder
--

aesonOptions :: Bool -> Bool -> A.SumEncoding -> A.Options
aesonOptions allNullary omitNothing sumEncoding = A.defaultOptions
    { A.allNullaryToStringTag = allNullary
    , A.omitNothingFields = omitNothing
    , A.sumEncoding = sumEncoding
    }

optPatterns :: [A.Options]
optPatterns =
    [ aesonOptions True True
    , aesonOptions True False
    , aesonOptions False True
    , aesonOptions False False
    ]
    <*>
    [ A.defaultTaggedObject
    , A.ObjectWithSingleField
    , A.TwoElemArray
    ]

#if MIN_VERSION_aeson(1,0,0)
encode :: (Generic a, A.GToJSON A.Zero (Rep a)) => A.Options -> a -> BL.ByteString
#else
encode :: (Generic a, A.GToJSON (Rep a)) => A.Options -> a -> BL.ByteString
#endif
encode opt a = A.encode (A.genericToJSON opt a)

--
-- Print values as json in python
--

optToStr :: String -> A.Options -> String
optToStr symbol aopts =
    "# " ++ symbol ++ " (allNullaryToStringTag: " ++ show a
                   ++ ", omitNothingFields: " ++ show b
                   ++ ", sumEncoding: " ++ showC c ++ ")"
  where
    a = A.allNullaryToStringTag aopts
    b = A.omitNothingFields aopts
    c = A.sumEncoding aopts

showC :: A.SumEncoding -> String
showC A.TwoElemArray = "array"
showC A.ObjectWithSingleField = "object"
showC _ = "tag"

pairsOptSymbol :: [A.Options] -> String -> [(A.Options, String)]
pairsOptSymbol opts name = go opts (1 :: Int)
  where
    go []          _ = []
    go (opt:opts') n = (opt, name ++ "_" ++ show n) : go opts' (n + 1)

#if MIN_VERSION_aeson(1,0,0)
printValueAsJson :: (Generic a, A.GToJSON A.Zero (Rep a)) => Handle -> [A.Options] -> String -> a -> IO ()
#else
printValueAsJson :: (Generic a, A.GToJSON (Rep a)) => Handle -> [A.Options] -> String -> a -> IO ()
#endif
printValueAsJson h opts name value =
    forM_ (pairsOptSymbol opts name) . uncurry $ \opt symbol -> do
        hPutStrLn  h $ optToStr symbol opt
        hPutStr    h $ symbol ++ " = json.loads('"
        BL.hPutStr h $ Main.encode opt value
        hPutStrLn  h "')"
        hPutStrLn  h ""

printValueAsJsonInPython :: FilePath -> IO ()
printValueAsJsonInPython path =
    withFile path WriteMode $ \h -> do
        hPutStrLn h "# -*- coding: utf-8 -*-"
        hPutStrLn h "import json"
        hPutStrLn h ""
        forM_ testData $ \(TestDatum name value) ->
            printValueAsJson h optPatterns name value

--
-- Print type definitions as schema in individualy json files
--

printTypeAsSchema :: (Generic a, G.JSONSchemaGen a, SchemaName (Rep a))
                  => FilePath -> G.Options -> [A.Options] -> Proxy a -> IO ()
printTypeAsSchema dir opts aoptss a =
    forM_ aoptss $ \aopts -> do
        let fa = fmap from a
        let filename = schemaName opts aopts fa
        let suffix = "." ++ schemaSuffix opts aopts fa
        let path = dir ++ "/" ++ filename
        let opts' = opts { G.schemaIdSuffix = suffix }
        withFile path WriteMode $ \h ->
            BL.hPutStrLn h $ G.generate' opts' aopts a

class SchemaName f where
    schemaName :: G.Options -> A.Options -> Proxy (f a) -> FilePath
    schemaSuffix :: G.Options -> A.Options -> Proxy (f a) -> String

instance (Datatype d) => SchemaName (D1 d f) where
    schemaName opts aopts p = modName ++ "." ++ typName ++ "." ++ schemaSuffix opts aopts p
      where
        modName = moduleName (undefined :: D1 d f p)
        typName = datatypeName (undefined :: D1 d f p)
    schemaSuffix opts aopts _ =
        show a ++ "." ++ show b ++ "." ++ showC c ++ G.schemaIdSuffix opts
      where
        a = A.allNullaryToStringTag aopts
        b = A.omitNothingFields aopts
        c = A.sumEncoding aopts

schemaOptions :: G.Options
schemaOptions = G.defaultOptions
    { G.baseUri = "https://github.com/yuga/jsonschema-gen/tests/"
    , G.schemaIdSuffix = ".json"
    , G.typeRefMap = fromList
        [ (typeOf (undefined :: RecordType2),  "https://github.com/yuga/jsonschema-gen/tests/Types.RecordType2.True.False.tag.json")
        , (typeOf (undefined :: ProductType2), "https://github.com/yuga/jsonschema-gen/tests/Types.ProductType2.True.False.tag.json")
        ]
    }

schemaOptions' :: G.Options
schemaOptions'= schemaOptions
    { G.typeRefMap = fromList
        [ (typeOf (undefined :: RecordType2),  "https://github.com/yuga/jsonschema-gen/tests/Types.RecordType2.True.False.tag.json")
        , (typeOf (undefined :: ProductType2), "https://github.com/yuga/jsonschema-gen/tests/Types.ProductType2.True.False.tag.json")
        , (typeOf (undefined :: UnitType2),    "https://github.com/yuga/jsonschema-gen/tests/Types.UnitType2.True.False.tag.json")
        ]
    , G.fieldTypeMap = fromList  [("recordField1A", G.FieldType (Proxy :: Proxy UnitType2))]
    }

printTypeAsSchemaInJson :: FilePath -> IO ()
printTypeAsSchemaInJson dir = do
    printTypeAsSchema dir schemaOptions' optPatterns (Proxy :: Proxy RecordType1)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy RecordType2)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy ProductType1)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy ProductType2)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy UnitType1)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy UnitType2)
    printTypeAsSchema dir schemaOptions  optPatterns (Proxy :: Proxy MixType1)

--
-- Print jsonschema validator in python
--

convertToPythonLoadingSchema :: (Generic a, SchemaName (Rep a)) => G.Options -> [A.Options] -> Proxy a -> ([String], [String])
convertToPythonLoadingSchema opts aoptss a =
    let fa = fmap from a
        toLoader aopts =
            let filename = schemaName opts aopts fa
                symbol = "schema_" ++ map dotToLowline filename
            in (symbol ++ " = json.load(codecs.open(schemaPath + '" ++ filename ++ "', 'r', 'utf-8'))")
        toStore aopts =
            let filename = schemaName opts aopts fa
                symbol = "schema_" ++ map dotToLowline filename
            in ("'" ++ G.baseUri opts ++ filename ++ "' : " ++ symbol)
    in (map toLoader &&& map toStore) aoptss

dotToLowline :: Char -> Char
dotToLowline '.' = '_'
dotToLowline c   = c

printLoadSchemas :: Handle -> IO ()
printLoadSchemas h = do
    let (loader, store) = convertToPythonLoadingSchema schemaOptions' optPatterns (Proxy :: Proxy RecordType1)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy RecordType2)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy ProductType1)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy ProductType2)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy UnitType1)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy UnitType2)
                       <> convertToPythonLoadingSchema schemaOptions  optPatterns (Proxy :: Proxy MixType1)
    hPutStrLn h "schemaPath = os.path.dirname(os.path.realpath(__file__)) + '/'"
    mapM_ (hPutStrLn h) loader
    hPutStrLn h ""
    mapM_ (hPutStrLn h . concat) . chunk 2 $ beginmap : (List.intersperse comma store) ++ [endofmap]
  where
    chunk n = takeWhile (not . null) . map (take n) . iterate (drop n)
    beginmap = "selfStore = { "
    comma    = "            , "
    endofmap = "            }"

printValidate :: Handle -> [A.Options] -> IO ()
printValidate h aoptss = do
    hPutStrLn h "def mkValidator(schema):"
    hPutStrLn h "    resolver = jsonschema.RefResolver(schema[u'id'], schema, store=selfStore)"
    hPutStrLn h "    validator = jsonschema.Draft4Validator(schema, resolver=resolver)"
    hPutStrLn h "    return validator"
    hPutStrLn h ""
    forM_ testData $ \(TestDatum name value) ->
        forM_ (pairsOptSymbol aoptss name) . uncurry $ \aopts dataSymbol -> do
            let schemaFilename = schemaName schemaOptions aopts (fmap from . pure $ value)
            let schemaSymbol  = map dotToLowline schemaFilename
            hPutStrLn h $ "mkValidator(" ++ "schema_" ++ schemaSymbol ++ ").validate(jsondata." ++ dataSymbol ++ ")"

printValidatorInPython :: FilePath -> IO ()
printValidatorInPython path =
    withFile path WriteMode $ \h -> do
        hPutStrLn h "# -*- coding: utf-8 -*-"
        hPutStrLn h "import codecs"
        hPutStrLn h "import json"
        hPutStrLn h "import jsondata"
        hPutStrLn h "import jsonschema"
        hPutStrLn h "import os"
        hPutStrLn h ""
        printLoadSchemas h
        hPutStrLn h ""
        printValidate h optPatterns

--
-- Run Test
--

pythonProcess :: FilePath -> CreateProcess
pythonProcess dir = (proc "python" [dir ++ "/jsonvalidator.py"])
    { std_in = CreatePipe
#if MIN_VERSION_process(1,2,0)
    , delegate_ctlc = True
#endif
    }

runTest :: FilePath -> IO ()
runTest dir = do
    handles <- createProcess $ pythonProcess dir
    case handles of
        (Just hIn, _, _, hP) -> do
            hClose hIn
            ec <- waitForProcess hP
            exitWith ec
        _ -> fail $ "Failed to launch python"

--
-- Main
--

main :: IO ()
main = do
    let dir = "tests"
    printValueAsJsonInPython (dir ++ "/jsondata.py")
    printTypeAsSchemaInJson dir
    printValidatorInPython (dir ++ "/jsonvalidator.py")
    ec <- system "python --version"
    case ec of
        ExitSuccess -> runTest dir
        _ -> putStrLn "If you have 'python' in your PATH, this test runs jsonvalidator.py"

