{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}

module AutoTypes.Unify
  ( T (..),
    toString,
    fromJson,
    unify1,
  )
where

import Control.Arrow
import Control.Exception (Exception, throw)
import qualified Data.Aeson as A
import qualified Data.Aeson.Encode.Pretty as A
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import Data.Aeson.QQ
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.List (intercalate, nub)
import Data.Map (Map)
import qualified Data.Map as M
import qualified Data.Map.Merge.Lazy as M
import Data.Maybe
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Time.Clock
import Data.Time.Format.ISO8601
import qualified Data.Vector as V
import Debug.Trace
import System.FilePath (takeDirectory)
import Prelude hiding (null)

data ScalarType
  = String
  | Number
  | DateTime
  | Bool
  deriving (Eq, Ord, Show)

scalarTypeString :: ScalarType -> String
scalarTypeString String = "string"
scalarTypeString Number = "number"
scalarTypeString DateTime = "datetime"
scalarTypeString Bool = "bool"

data T
  = List (Maybe T)
  | Object (Map String T)
  | Option (Maybe T)
  | Scalar ScalarType
  | Union (S.Set T)
  | Reference String
  deriving (Eq, Ord, Show)

instance A.ToJSON T where
  toJSON (List t) = A.object [K.fromString "type" A..= "array", K.fromString "items" A..= A.toJSON t]
  toJSON (Object ts) = A.object [K.fromString "type" A..= "object", K.fromString "properties" A..= A.toJSON ts]
  toJSON (Option t) = A.toJSON t
  toJSON (Scalar DateTime) = A.object [K.fromString "type" A..= "string", K.fromString "format" A..= "date-time"]
  toJSON (Scalar String) = A.object [K.fromString "type" A..= "string"]
  toJSON (Scalar Bool) = A.object [K.fromString "type" A..= "bool"]
  toJSON (Scalar Number) = A.object [K.fromString "type" A..= "number"]
  toJSON (Union ts) = A.object [K.fromString "oneOf" A..= A.toJSON ts]
  toJSON (Reference i) = A.object [K.fromString "$ref" A..= A.String (T.pack i)]

toString :: T -> String
toString = BL.unpack . A.encodePretty . A.toJSON

union :: S.Set T -> T
union ts
  | S.size ts == 1 = S.findMin ts
  | otherwise = Union ts

unify1 :: T -> T -> [T]
unify1 (Scalar n) (Scalar m)
  | n == m = [Scalar n]
  | otherwise = [union (S.fromList [Scalar n, Scalar m])]
unify1 (Reference i) (Reference j)
  | i == j = [Reference i]
  | otherwise = [union (S.fromList [Reference i, Reference j])]
unify1 l@(Object ls) r@(Object rs) =
  let os =
        ( map Object . traverse id $
            let f _ (l@(Option _)) = [l]
                f _ l = [Option (Just l)]
             in M.merge
                  (M.mapMissing f)
                  (M.mapMissing f)
                  (M.zipWithMatched (\_ l r -> unify1 l r))
                  ls
                  rs
        )
   in os
        ++ ( if l `subst` r || r `subst` l
               then []
               else [union (S.fromList [l, r])]
           )
unify1 (Option Nothing) (Option Nothing) = [Option Nothing]
unify1 (Option (Just l)) (Option Nothing) = [Option (Just l)]
unify1 (Option Nothing) (Option (Just r)) = [Option (Just r)]
unify1 (Option (Just l)) (Option (Just r)) = map (Option . Just) (unify1 l r)
unify1 (Option Nothing) r = [Option (Just r)]
unify1 (List Nothing) (List Nothing) = [List Nothing]
unify1 (List Nothing) (List (Just t)) = [List (Just t)]
unify1 (List (Just t)) (List Nothing) = [List (Just t)]
unify1 (List (Just l)) (List (Just r)) =
  if l == r || r `subst` l
    then [List (Just l)]
    else
      if l `subst` r
        then [List (Just r)]
        else [List (Just (union (S.fromList [l, r])))]
unify1 l (Option Nothing) = [Option (Just l)]
unify1 l (Option (Just r)) = map (Option . Just) (unify1 l r)
unify1 (Option (Just l)) r = map (Option . Just) (unify1 l r)
unify1 (Union ls) (Union rs) = [union (ls `S.union` rs)]
unify1 (Union ls) r = [union (S.insert r ls)]
unify1 l (Union rs) = [union (S.insert l rs)]
unify1 l r = [union (S.fromList [l, r])]

subst :: T -> T -> Bool
subst (Object l) (Object r) =
  and
    ( map
        ( \(k, t) -> case (t, M.lookup k r) of
            (t, Just t') -> t `subst` t'
            _ -> False
        )
        (M.toList l)
    )
subst (Option Nothing) (Option _) = True
subst l (Option Nothing) = True
subst (Option (Just l)) (Option (Just r)) = l `subst` r
subst l (Option (Just r)) = l `subst` r
subst l r = l == r

lims :: [T] -> [T]
lims ts = nub [t | t <- ts, all (\t' -> not (t `subst` t') || t' == t) ts]

unify l r =
  let s =
        unlines
          ( [ toString l ++ "," ++ toString r,
              "---"
            ]
              ++ map toString t
          )
      t = unify1 l r
   in Debug.Trace.trace s t

{-
unify :: T -> T -> Either (T, T) T
unify (Scalar n) (Scalar m)
  | n == m = Right (Scalar n)
  | otherwise = Left (Scalar n, Scalar m)
unify (Object ls) (Object rs) =
  let f _ (l@(Option _)) = Right l
      f _ l = Right (Option (Just l))
   in case M.merge
        (M.mapMissing f)
        (M.mapMissing f)
        (M.zipWithMatched (\_ l r -> unify l r))
        ls
        rs
        & M.partition isRight
        & ( \(rs, ls) ->
              (M.map (fromRight undefined) rs, M.elems (M.map (fromLeft undefined) ls))
          ) of
        (_, e : _) -> Left e
        (lrs, []) -> Right (Object lrs)
unify (Option Nothing) (Option Nothing) = Right (Option Nothing)
unify (Option (Just l)) (Option Nothing) = Right (Option (Just l))
unify (Option Nothing) (Option (Just r)) = Right (Option (Just r))
unify (Option (Just l)) (Option (Just r)) = Option . Just <$> unify l r
-}

object :: Map String T -> Maybe T
object = Just . Object

list :: Maybe T -> Maybe T
list = Just . List

string, number, bool, dateTime, null :: Maybe T
string = Just (Scalar String)
number = Just (Scalar Number)
bool = Just (Scalar Bool)
dateTime = Just (Scalar DateTime)
null = Nothing

data InferException = InferException [T]
  deriving (Show)

instance Exception InferException

fromJson :: A.Value -> Maybe T
fromJson (A.Object kvs) =
  case map (first K.toString) (KM.toList kvs) of
    [("$ref", A.String i)] -> Just (Reference (takeDirectory (T.unpack i)))
    _ -> object (M.mapKeys K.toString (M.mapMaybe fromJson (KM.toMap kvs)))
fromJson t@(A.Array vs) =
  let ts = mapMaybe fromJson (V.toList vs)
   in case nub ts of
        [] -> list Nothing
        [t] -> list (Just t)
        _ -> throw (InferException ts)
fromJson (A.String s)
  | Just _ <- iso8601ParseM @Maybe @UTCTime (T.unpack s) = dateTime
  | otherwise = string
fromJson (A.Number _) = number
fromJson (A.Bool _) = bool
fromJson A.Null = null

{-
object1 =
  [aesonQQ|{
    "firstName": "firstName",
    "lastName": "lastName"
  }|]

object2 =
  [aesonQQ|{
    "firstName": "firstName",
    "lastName": "lastName",
    "birthDay": null
  }|]

object3 =
  [aesonQQ|{
    "firstName": "firstName",
    "lastName": "lastName",
    "birthDay": "1990-01-01"
  }|]

object4 =
  [aesonQQ|{
  "firstName": "firstName"
  }|]

object5 =
  [aesonQQ|{
  "lastName": 42,
  "birthDay": null
  }|]

main =
  -- fromJson object2 =:= fromJson object3
  -- unify (fromJson object1) (fromJson object2)
  putStrLn
    ( intercalate
        "\n\n"
        ( map
            toString
            ( foldr1
                (\ls rs -> (concat [unify1 l r | l <- ls, r <- rs]))
                ( map
                    ((: []) . fromJson)
                    [ object1,
                      object2,
                      -- object3,
                      -- object4,
                      object5
                    ]
                )
            )
        )
    )-}

-- >>= unify (fromJson object2)
-- >>= unify (fromJson object4)