{-# LANGUAGE OverloadedStrings, FlexibleInstances, TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module HsDev.Database.SQLite.Instances (
	JSON(..)
	) where

import Control.Lens ((^.), (^?), _Just)
import Data.Aeson as A hiding (Error)
import Data.Maybe
import Data.Foldable
import Data.Time.Clock.POSIX
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString.Lazy as L
import Database.SQLite.Simple
import Database.SQLite.Simple.ToField
import Database.SQLite.Simple.FromField
import Language.Haskell.Extension
import qualified Language.Haskell.Names as N
import qualified Language.Haskell.Exts as H
import Text.Format

import System.Directory.Paths
import HsDev.Symbols.Name
import HsDev.Symbols.Location
import HsDev.Symbols.Types
import HsDev.Tools.Ghc.Types
import HsDev.Tools.Types
import HsDev.Util

instance ToField Value where
	toField = SQLBlob . L.toStrict . encode

instance FromField Value where
	fromField fld = case fieldData fld of
		SQLText s -> either fail return . eitherDecode . L.fromStrict . T.encodeUtf8 $ s
		SQLBlob s -> either fail return . eitherDecode . L.fromStrict $ s
		_ -> fail "invalid json field type"

newtype JSON a = JSON { getJSON :: a }
	deriving (Eq, Ord, Read, Show)

instance ToJSON a => ToField (JSON a) where
	toField = SQLBlob . L.toStrict . encode . getJSON

instance FromJSON a => FromField (JSON a) where
	fromField fld = case fieldData fld of
		SQLText s -> either fail (return . JSON) . eitherDecode . L.fromStrict . T.encodeUtf8 $ s
		SQLBlob s -> either fail (return . JSON) . eitherDecode . L.fromStrict $ s
		_ -> fail "invalid json field type"

instance FromRow Position where
	fromRow = Position <$> field <*> field

instance ToRow Position where
	toRow (Position l c) = [toField l, toField c]

instance FromRow Region where
	fromRow = Region <$> fromRow <*> fromRow

instance ToRow Region where
	toRow (Region f t) = toRow f ++ toRow t

instance FromRow ModulePackage where
	fromRow = ModulePackage <$> field <*> (fromMaybe T.empty <$> field)

instance ToRow ModulePackage where
	toRow (ModulePackage name ver) = [toField name, toField ver]

instance FromRow ModuleLocation where
	fromRow = do
		file <- field
		cabal <- field
		dirs <- field
		pname <- field
		pver <- field
		iname <- field
		iexposed <- field
		other <- field

		maybe (fail $ "Can't parse module location: {}" ~~ show (file, cabal, dirs, pname, pver, iname, iexposed, other)) return $ msum [
			FileModule <$> file <*> pure (project <$> cabal),
			InstalledModule <$> maybe (pure []) fromJSON' dirs <*> (ModulePackage <$> pname <*> pver) <*> iname <*> iexposed,
			OtherLocation <$> other,
			pure NoLocation]

instance ToRow ModuleLocation where
	toRow mloc = [
		toField $ mloc ^? moduleFile,
		toField $ mloc ^? moduleProject . _Just . projectCabal,
		toField $ fmap toJSON $ mloc ^? moduleInstallDirs,
		toField $ mloc ^? modulePackage . packageName,
		toField $ mloc ^? modulePackage . packageVersion,
		toField $ mloc ^? installedModuleName,
		toField $ mloc ^? installedModuleExposed,
		toField $ mloc ^? otherLocationName]

instance FromRow ModuleId where
	fromRow = ModuleId <$> field <*> fromRow

instance ToRow ModuleId where
	toRow mid = toField (mid ^. moduleName) : toRow (mid ^. moduleLocation)

instance FromRow Import where
	fromRow = Import <$> fromRow <*> field <*> field <*> field

instance ToRow Import where
	toRow (Import p n q a) = toRow p ++ [toField n, toField q, toField a]

instance FromRow SymbolId where
	fromRow = SymbolId <$> field <*> fromRow

instance ToRow SymbolId where
	toRow (SymbolId nm mid) = toField nm : toRow mid

instance FromRow SymbolInfo where
	fromRow = do
		what <- field @String
		ty <- field
		parent <- field
		ctors <- field
		args <- field
		ctx <- field
		assoc <- field
		patTy <- field
		patCtor <- field
		maybe (fail $ "Can't parse symbol info: {}" ~~ show (what, ty, parent, ctors, args, ctx, assoc, patTy, patCtor)) return $ case what of
			"function" -> return $ Function ty
			"method" -> Method <$> pure ty <*> parent
			"selector" -> Selector <$> pure ty <*> parent <*> (fromJSON' =<< ctors)
			"ctor" -> Constructor <$> (fromJSON' =<< args) <*> parent
			"type" -> Type <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx)
			"newtype" -> NewType <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx)
			"data" -> Data <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx)
			"class" -> Class <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx)
			"type-family" -> TypeFam <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx) <*> pure assoc
			"data-family" -> DataFam <$> (fromJSON' =<< args) <*> (fromJSON' =<< ctx) <*> pure assoc
			"pat-ctor" -> PatConstructor <$> (fromJSON' =<< args) <*> pure patTy
			"pat-selector" -> PatSelector <$> pure ty <*> pure patTy <*> patCtor
			_ -> Nothing

instance ToRow SymbolInfo where
	toRow si = [
		toField $ symbolInfoType si,
		toField $ si ^? functionType . _Just,
		toField $ msum [si ^? parentClass, si ^? parentType],
		toField $ toJSON $ si ^? selectorConstructors,
		toField $ toJSON $ si ^? typeArgs,
		toField $ toJSON $ si ^? typeContext,
		toField $ si ^? familyAssociate . _Just,
		toField $ si ^? patternType . _Just,
		toField $ si ^? patternConstructor]

instance FromRow Symbol where
	fromRow = Symbol <$> fromRow <*> field <*> pos <*> fromRow where
		pos = do
			line <- field
			column <- field
			return $ Position <$> line <*> column

instance ToRow Symbol where
	toRow sym = concat [
		toRow (sym ^. symbolId),
		[toField $ sym ^. symbolDocs],
		maybe [SQLNull, SQLNull] toRow (sym ^. symbolPosition),
		toRow (sym ^. symbolInfo)]

instance FromRow a => FromRow (Scoped a) where
	fromRow = flip Scoped <$> fromRow <*> field

instance ToRow a => ToRow (Scoped a) where
	toRow (Scoped q s) = toRow s ++ [toField q]

instance ToRow Project where
	toRow (Project name _ cabal pdesc dbs) = [
		toField name,
		toField cabal,
		toField $ pdesc ^? _Just . projectVersion,
		toField dbs]

instance FromRow Project where
	fromRow = do
		name <- field
		cabal <- field
		ver <- field
		dbs <- field
		return $ Project name (takeDir cabal) cabal (fmap (\v -> ProjectDescription v Nothing [] []) ver) dbs

instance FromRow Library where
	fromRow = do
		mods <- field >>= maybe (fail "Error parsing library modules") return . fromJSON'
		binfo <- fromRow
		return $ Library mods binfo

instance FromRow Executable where
	fromRow = Executable <$> field <*> field <*> fromRow

instance FromRow Test where
	fromRow = Test <$> field <*> field <*> field <*> fromRow

instance FromRow Info where
	fromRow = Info <$>
		(field >>= maybe (fail "Error parsing depends") return . fromJSON') <*>
		field <*>
		(field >>= maybe (fail "Error parsing extensions") return . fromJSON') <*>
		(field >>= maybe (fail "Error parsing ghc-options") return . fromJSON') <*>
		(field >>= maybe (fail "Error parsing source-dirs") return . fromJSON') <*>
		(field >>= maybe (fail "Error parsing other-modules") return . fromJSON')

instance FromField Language where
	fromField fld = case fieldData fld of
		SQLText txt -> parseDT "Language" (T.unpack txt)
		_ -> fail "Can't parse language, invalid type"

instance ToField PackageDb where
	toField GlobalDb = toField ("global-db" :: String)
	toField UserDb = toField ("user-db" :: String)
	toField (PackageDb p) = toField ("package-db:" ++ T.unpack p)

instance FromField PackageDb where
	fromField fld = do
		s <- fromField fld
		case s of
			"global-db" -> return GlobalDb
			"user-db" -> return UserDb
			_ -> case T.stripPrefix "package-db:" s of
				Just p' -> return $ PackageDb p'
				Nothing -> fail $ "Can't parse package-db, invalid string: " ++ T.unpack s

instance ToField PackageDbStack where
	toField = toField . toJSON . packageDbs

instance FromField PackageDbStack where
	fromField = fromField >=> maybe (fail "Error parsing package-db-stack") (return . mkPackageDbStack) . fromJSON'

instance FromRow SymbolUsage where
	fromRow = SymbolUsage <$> fromRow <*> field <*> fromRow <*> fromRow

instance FromField POSIXTime where
	fromField = fmap (fromRational . toRational @Double) . fromField

instance ToField POSIXTime where
	toField = toField . fromRational @Double . toRational

instance FromRow Inspection where
	fromRow = do
		tm <- field
		opts <- field
		case (tm, opts) of
			(Nothing, Nothing) -> return InspectionNone
			(_, Just opts') -> InspectionAt (fromMaybe 0 tm) <$>
				maybe (fail "Error parsing inspection opts") return (fromJSON' opts')
			(Just _, Nothing) -> fail "Error parsing inspection data, time is set, but flags are null"

instance ToRow Inspection where
	toRow InspectionNone = [SQLNull, SQLNull]
	toRow (InspectionAt tm opts) = [
		if tm == 0 then SQLNull else toField tm,
		toField $ toJSON opts]

instance FromRow TypedExpr where
	fromRow = TypedExpr <$> field <*> field

instance ToRow TypedExpr where
	toRow (TypedExpr e t) = [toField e, toField t]

instance FromField (H.Name ()) where
	fromField = fmap toName_ . fromField

instance ToField (H.Name ()) where
	toField = toField . fromName_

instance FromField (H.ModuleName ()) where
	fromField = fmap toModuleName_ . fromField

instance ToField (H.ModuleName ()) where
	toField = toField . fromModuleName_

instance FromRow N.Symbol where
	fromRow = do
		what <- field @T.Text
		mname <- field
		name <- field
		parent <- field
		ctors <- do
			ctorsJson <- field
			return $ fmap (map toName_) (ctorsJson >>= fromJSON')
		assoc <- field
		patType <- field
		patCtor <- field
		let
			m = toModuleName_ mname
			n = toName_ name
		maybe (fail $ "Can't parse symbol: {}" ~~ show (what, mname, name, parent, ctors, assoc, patType, patCtor)) return $ case what of
			"function" -> return $ N.Value m n
			"method" -> N.Method m n <$> parent
			"selector" -> N.Selector m n <$> parent <*> ctors
			"ctor" -> N.Constructor m n <$> parent
			"type" -> return $ N.Type m n
			"newtype" -> return $ N.NewType m n
			"data" -> return $ N.Data m n
			"class" -> return $ N.Class m n
			"type-family" -> return $ N.TypeFam m n assoc
			"data-family" -> return $ N.DataFam m n assoc
			"pat-ctor" -> return $ N.PatternConstructor m n patType
			"pat-selector" -> N.PatternSelector m n patType <$> patCtor
			_ -> Nothing

instance ToRow N.Symbol where
	toRow = padNulls 8 . toRow' where
		toRow' (N.Value m n) = mk "function" [toField m, toField n]
		toRow' (N.Method m n p) = mk "method" [toField m, toField n, toField p]
		toRow' (N.Selector m n p cs) = mk "selector" [toField m, toField n, toField p, toField $ toJSON (map fromName_ cs)]
		toRow' (N.Constructor m n p) = mk "ctor" [toField m, toField n, toField p]
		toRow' (N.Type m n) = mk "type" [toField m, toField n]
		toRow' (N.NewType m n) = mk "newtype" [toField m, toField n]
		toRow' (N.Data m n) = mk "data" [toField m, toField n]
		toRow' (N.Class m n) = mk "class" [toField m, toField n]
		toRow' (N.TypeFam m n assoc) = mk "type-family" [toField m, toField n, SQLNull, SQLNull, toField assoc]
		toRow' (N.DataFam m n assoc) = mk "data-family" [toField m, toField n, SQLNull, SQLNull, toField assoc]
		toRow' (N.PatternConstructor m n pty) = mk "pat-ctor" [toField m, toField n, SQLNull, SQLNull, SQLNull, toField pty]
		toRow' (N.PatternSelector m n pty pctor) = mk "pat-selector" [toField m, toField n, SQLNull, SQLNull, SQLNull, toField pty, toField pctor]

		mk :: T.Text -> [SQLData] -> [SQLData]
		mk what = (toField what :)
		padNulls n fs = fs ++ replicate (n - length fs) SQLNull

instance FromField Severity where
	fromField fld = do
		s <- fromField @String fld
		msum [
			guard (s == "error") >> return Error,
			guard (s == "warning") >> return Warning,
			guard (s == "hint") >> return Hint,
			fail ("Unknown severity: {}" ~~ s)]

instance ToField Severity where
	toField Error = toField @String "error"
	toField Warning = toField @String "warning"
	toField Hint = toField @String "hint"

instance FromRow OutputMessage where
	fromRow = OutputMessage <$> field <*> field

instance ToRow OutputMessage where
	toRow (OutputMessage msg suggest) = [toField msg, toField suggest]

instance FromRow a => FromRow (Note a) where
	fromRow = Note <$> (FileModule <$> field <*> pure Nothing) <*> fromRow <*> field <*> fromRow

instance ToRow a => ToRow (Note a) where
	toRow (Note mloc rgn sev n) = concat [
		[toField $ mloc ^? moduleFile],
		toRow rgn,
		[toField sev],
		toRow n]
