module Rattletrap.Type.ClassAttributeMap
  ( ClassAttributeMap(..)
  , classHasLocation
  , classHasRotation
  , getAttributeIdLimit
  , getAttributeMap
  , getAttributeName
  , getClassName
  , getName
  , getObjectName
  , makeClassAttributeMap
  )
where

import Rattletrap.Data
import Rattletrap.Type.AttributeMapping
import Rattletrap.Type.Cache
import Rattletrap.Type.ClassMapping
import Rattletrap.Type.Common
import Rattletrap.Type.CompressedWord
import Rattletrap.Type.List
import Rattletrap.Type.Str
import Rattletrap.Type.Word32le

import qualified Data.IntMap as IntMap
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Tuple as Tuple

-- | This data structure holds all the information about classes, objects, and
-- attributes in the replay. The class hierarchy is not fixed; it is encoded
-- in the 'Rattletrap.Content.Content'. Similarly, the attributes that belong
-- to each class are not fixed either. Converting the raw data into a usable
-- structure is tedious; see 'makeClassAttributeMap'.
data ClassAttributeMap = ClassAttributeMap
  { classAttributeMapObjectMap :: Map Word32le Str
  -- ^ A map from object IDs to their names.
  , classAttributeMapObjectClassMap :: Map Word32le Word32le
  -- ^ A map from object IDs to their class IDs.
    , classAttributeMapValue :: Map Word32le (Map Word32le Word32le)
  -- ^ A map from class IDs to a map from attribute stream IDs to attribute
  -- IDs.
  , classAttributeMapNameMap :: IntMap.IntMap Str
  } deriving (Eq, Ord, Show)

type Bimap l r = (Map l r, Map r l)

bimap :: (Ord l, Ord r) => [(l, r)] -> Bimap l r
bimap xs = (Map.fromList xs, Map.fromList (fmap Tuple.swap xs))

lookupL :: Ord l => l -> Bimap l r -> Maybe r
lookupL k = Map.lookup k . fst

lookupR :: Ord r => r -> Bimap l r -> Maybe l
lookupR k = Map.lookup k . snd

-- | Makes a 'ClassAttributeMap' given the necessary fields from the
-- 'Rattletrap.Content.Content'.
makeClassAttributeMap
  :: List Str
  -- ^ From 'Rattletrap.Content.contentObjects'.
  -> List ClassMapping
  -- ^ From 'Rattletrap.Content.contentClassMappings'.
  -> List Cache
  -- ^ From 'Rattletrap.Content.contentCaches'.
  -> List Str
  -- ^ From 'Rattletrap.Content.contentNames'.
  -> ClassAttributeMap
makeClassAttributeMap objects classMappings caches names =
  let
    objectMap = makeObjectMap objects
    classMap = makeClassMap classMappings
    objectClassMap = makeObjectClassMap objectMap classMap
    classCache = makeClassCache classMap caches
    attributeMap = makeAttributeMap caches
    classIds = fmap (\(_, classId, _, _) -> classId) classCache
    parentMap = makeParentMap classCache
    value = Map.fromList
      (fmap
        (\classId ->
          let
            ownAttributes =
              Maybe.fromMaybe Map.empty (Map.lookup classId attributeMap)
            parentsAttributes = case Map.lookup classId parentMap of
              Nothing -> []
              Just parentClassIds -> fmap
                (\parentClassId -> Maybe.fromMaybe
                  Map.empty
                  (Map.lookup parentClassId attributeMap)
                )
                parentClassIds
            attributes = ownAttributes : parentsAttributes
          in (classId, Map.fromList (concatMap Map.toList attributes))
        )
        classIds
      )
    nameMap = makeNameMap names
  in ClassAttributeMap objectMap objectClassMap value nameMap

makeNameMap :: List Str -> IntMap.IntMap Str
makeNameMap names = IntMap.fromDistinctAscList (zip [0 ..] (listValue names))

getName :: IntMap.IntMap Str -> Word32le -> Maybe Str
getName nameMap nameIndex =
  IntMap.lookup (fromIntegral (word32leValue nameIndex)) nameMap

makeObjectClassMap
  :: Map Word32le Str -> Bimap Word32le Str -> Map Word32le Word32le
makeObjectClassMap objectMap classMap = do
  let objectIds = Map.keys objectMap
  let classIds = fmap (getClassId objectMap classMap) objectIds
  let rawPairs = zip objectIds classIds
  let
    pairs = Maybe.mapMaybe
      (\(objectId, maybeClassId) -> case maybeClassId of
        Nothing -> Nothing
        Just classId -> Just (objectId, classId)
      )
      rawPairs
  Map.fromList pairs

getClassId
  :: Map Word32le Str -> Bimap Word32le Str -> Word32le -> Maybe Word32le
getClassId objectMap classMap objectId = do
  objectName <- getObjectName objectMap objectId
  className <- getClassName objectName
  lookupR className classMap

makeClassCache
  :: Bimap Word32le Str
  -> List Cache
  -> [(Maybe Str, Word32le, Word32le, Word32le)]
makeClassCache classMap caches = fmap
  (\cache ->
    let classId = cacheClassId cache
    in
      ( lookupL classId classMap
      , classId
      , cacheCacheId cache
      , cacheParentCacheId cache
      )
  )
  (listValue caches)

makeClassMap :: List ClassMapping -> Bimap Word32le Str
makeClassMap classMappings = bimap
  (fmap
    (\classMapping ->
      (classMappingStreamId classMapping, classMappingName classMapping)
    )
    (listValue classMappings)
  )

makeAttributeMap :: List Cache -> Map Word32le (Map Word32le Word32le)
makeAttributeMap caches = Map.fromList
  (fmap
    (\cache ->
      ( cacheClassId cache
      , Map.fromList
        (fmap
          (\attributeMapping ->
            ( attributeMappingStreamId attributeMapping
            , attributeMappingObjectId attributeMapping
            )
          )
          (listValue (cacheAttributeMappings cache))
        )
      )
    )
    (listValue caches)
  )

makeShallowParentMap
  :: [(Maybe Str, Word32le, Word32le, Word32le)] -> Map Word32le Word32le
makeShallowParentMap classCache = Map.fromList
  (Maybe.mapMaybe
    (\xs -> case xs of
      [] -> Nothing
      (maybeClassName, classId, _, parentCacheId) : rest -> do
        parentClassId <- getParentClass maybeClassName parentCacheId rest
        pure (classId, parentClassId)
    )
    (List.tails (reverse classCache))
  )

makeParentMap
  :: [(Maybe Str, Word32le, Word32le, Word32le)] -> Map Word32le [Word32le]
makeParentMap classCache =
  let shallowParentMap = makeShallowParentMap classCache
  in
    Map.mapWithKey
      (\classId _ -> getParentClasses shallowParentMap classId)
      shallowParentMap

getParentClasses :: Map Word32le Word32le -> Word32le -> [Word32le]
getParentClasses shallowParentMap classId =
  case Map.lookup classId shallowParentMap of
    Nothing -> []
    Just parentClassId ->
      parentClassId : getParentClasses shallowParentMap parentClassId

getParentClass
  :: Maybe Str
  -> Word32le
  -> [(Maybe Str, Word32le, Word32le, Word32le)]
  -> Maybe Word32le
getParentClass maybeClassName parentCacheId xs = case maybeClassName of
  Nothing -> getParentClassById parentCacheId xs
  Just className -> getParentClassByName className parentCacheId xs

getParentClassById
  :: Word32le -> [(Maybe Str, Word32le, Word32le, Word32le)] -> Maybe Word32le
getParentClassById parentCacheId xs =
  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
    [] -> if parentCacheId == Word32le 0
      then Nothing
      else getParentClassById (Word32le (word32leValue parentCacheId - 1)) xs
    (_, parentClassId, _, _) : _ -> Just parentClassId

getParentClassByName
  :: Str
  -> Word32le
  -> [(Maybe Str, Word32le, Word32le, Word32le)]
  -> Maybe Word32le
getParentClassByName className parentCacheId xs =
  case Map.lookup className parentClasses of
    Nothing -> getParentClassById parentCacheId xs
    Just parentClassName -> Maybe.maybe
      (getParentClassById parentCacheId xs)
      Just
      (Maybe.listToMaybe
        (fmap
          (\(_, parentClassId, _, _) -> parentClassId)
          (filter
            (\(_, _, cacheId, _) -> cacheId <= parentCacheId)
            (filter
              (\(maybeClassName, _, _, _) ->
                maybeClassName == Just parentClassName
              )
              xs
            )
          )
        )
      )

parentClasses :: Map Str Str
parentClasses =
  Map.map toStr (Map.mapKeys toStr (Map.fromList rawParentClasses))

makeObjectMap :: List Str -> Map Word32le Str
makeObjectMap objects =
  Map.fromAscList (zip (fmap Word32le [0 ..]) (listValue objects))

getObjectName :: Map Word32le Str -> Word32le -> Maybe Str
getObjectName objectMap objectId = Map.lookup objectId objectMap

getClassName :: Str -> Maybe Str
getClassName rawObjectName =
  Map.lookup (normalizeObjectName rawObjectName) objectClasses

normalizeObjectName :: Str -> Str
normalizeObjectName objectName =
  let
    name = strValue objectName
    crowdActor = Text.pack "TheWorld:PersistentLevel.CrowdActor_TA"
    crowdManager = Text.pack "TheWorld:PersistentLevel.CrowdManager_TA"
    boostPickup = Text.pack "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
    mapScoreboard = Text.pack "TheWorld:PersistentLevel.InMapScoreboard_TA"
    breakout = Text.pack "TheWorld:PersistentLevel.BreakOutActor_Platform_TA"
  in if Text.isInfixOf crowdActor name
    then Str crowdActor
    else if Text.isInfixOf crowdManager name
      then Str crowdManager
      else if Text.isInfixOf boostPickup name
        then Str boostPickup
        else if Text.isInfixOf mapScoreboard name
          then Str mapScoreboard
          else if Text.isInfixOf breakout name
            then Str breakout
            else objectName

objectClasses :: Map Str Str
objectClasses =
  Map.map toStr (Map.mapKeys toStr (Map.fromList rawObjectClasses))

classHasLocation :: Str -> Bool
classHasLocation className = Set.member className classesWithLocation

classesWithLocation :: Set.Set Str
classesWithLocation = Set.fromList (fmap toStr rawClassesWithLocation)

classHasRotation :: Str -> Bool
classHasRotation className = Set.member className classesWithRotation

classesWithRotation :: Set.Set Str
classesWithRotation = Set.fromList (fmap toStr rawClassesWithRotation)

getAttributeIdLimit :: Map Word32le Word32le -> Maybe Word
getAttributeIdLimit attributeMap = do
  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
  pure (fromIntegral (word32leValue streamId))

getAttributeName
  :: ClassAttributeMap -> Map Word32le Word32le -> CompressedWord -> Maybe Str
getAttributeName classAttributeMap attributeMap streamId = do
  let key = Word32le (fromIntegral (compressedWordValue streamId))
  attributeId <- Map.lookup key attributeMap
  let objectMap = classAttributeMapObjectMap classAttributeMap
  Map.lookup attributeId objectMap

getAttributeMap
  :: ClassAttributeMap
  -> Map CompressedWord Word32le
  -> CompressedWord
  -> Maybe (Map Word32le Word32le)
getAttributeMap classAttributeMap actorMap actorId = do
  objectId <- Map.lookup actorId actorMap
  let objectClassMap = classAttributeMapObjectClassMap classAttributeMap
  classId <- Map.lookup objectId objectClassMap
  let value = classAttributeMapValue classAttributeMap
  Map.lookup classId value