module Utils.Allocation
  ( computeMatching
  , MatchingLog(..)
  , computeMatchingLog
  ) where

import Import.NoModel hiding (StateT, st, get)

import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Sequence as Seq

import Data.Array.ST (STArray)
import qualified Data.Array.MArray as MArr

import System.Random (RandomGen)
import Control.Monad.Trans.Random.Strict (evalRandT, RandT)
import Control.Monad.Trans.State.Strict (StateT, modify', get, gets, evalStateT)

import Control.Monad.ST

import Data.List ((!!))


type CourseIndex = Int
type StudentIndex = Int
type CloneIndex = Int

data MatchingLog student course cloneIndex
  = MatchingConsider
    { mlStudent :: student, mlClone :: cloneIndex }
  | MatchingApply
    { mlStudent :: student, mlClone :: cloneIndex, mlCourse :: course }
  | MatchingNoApplyCloneInstability
    { mlStudent :: student, mlClone :: cloneIndex, mlCourse :: course }
  | MatchingLostSpot
    { mlStudent :: student, mlClone :: cloneIndex, mlCourse :: course }
  deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance (NFData student, NFData course, NFData cloneIndex) => NFData (MatchingLog student course cloneIndex)

deriveJSON defaultOptions
  { constructorTagModifier = camelToPathPiece' 1
  , fieldLabelModifier = camelToPathPiece' 1
  } ''MatchingLog

computeMatching :: forall randomGen student course cloneCount cloneIndex capacity studentRatingCourse courseRatingStudent courseRatingStudent'.
                   ( RandomGen randomGen
                   , Ord student, Ord course
                   , NFData student
                   , Ord studentRatingCourse
                   , Ord courseRatingStudent'
                   , Integral cloneCount, Integral capacity, Integral cloneIndex
                   )
                => randomGen -- ^ Source of randomness
                -> Map student (cloneIndex, cloneCount) -- ^ requested number of placements per student
                -> Map course (Maybe capacity) -- ^ capacity of courses
                -> Map (student, course) (studentRatingCourse, courseRatingStudent) -- ^ Mutual preference ordering @(studentRatingCourse, courseRatingStudent)@
                -> (student -> cloneIndex -> courseRatingStudent -> courseRatingStudent') -- ^ Adjust preference ordering of courses (incorporate central priority)
                -> Set (student, course) -- ^ Stable matching
computeMatching g cloneCounts capacities preferences centralNudge
  = view _1 . runWriter $ computeMatchingLog g cloneCounts capacities preferences centralNudge

computeMatchingLog :: forall randomGen student course cloneCount cloneIndex capacity studentRatingCourse courseRatingStudent courseRatingStudent'.
                      ( RandomGen randomGen
                      , Ord student, Ord course
                      , NFData student
                      , Ord studentRatingCourse
                      , Ord courseRatingStudent'
                      , Integral cloneCount, Integral capacity, Integral cloneIndex
                      )
                   => randomGen -- ^ Source of randomness
                   -> Map student (cloneIndex, cloneCount) -- ^ requested number of placements and first cloneIndex per student
                   -> Map course (Maybe capacity) -- ^ capacity of courses
                   -> Map (student, course) (studentRatingCourse, courseRatingStudent) -- ^ Mutual preference ordering @(studentRatingCourse, courseRatingStudent)@
                   -> (student -> cloneIndex -> courseRatingStudent -> courseRatingStudent') -- ^ Adjust preference ordering of courses (incorporate central priority)
                   -> Writer (Seq (MatchingLog student course cloneIndex)) (Set (student, course)) -- ^ Stable matching
computeMatchingLog g cloneCounts capacities preferences centralNudge = writer $ runST computeMatching'
  where
    computeMatching' :: forall s. ST s (Set (student, course), Seq (MatchingLog student course cloneIndex))
    computeMatching' = runWriterT . flip evalRandT g $ do
      stb <- (Map.!) <$> sequence (Map.fromSet (const getRandom) clonedStudents) :: RandT randomGen (WriterT _ (ST s)) ((student, CloneIndex) -> UUID)
      cstb <- (Map.!) <$> sequence (Map.fromSet (const getRandom) courses) :: RandT randomGen (WriterT _ (ST s)) (course -> UUID)

      courses' <- lift . lift . MArr.newListArray courseBounds . map initCourse $ Set.toAscList courses :: RandT randomGen (WriterT _ (ST s)) (STArray s CourseIndex (Either (Set (student, CloneIndex)) (Seq (student, CloneIndex))))

      stPrefs <- lift . lift $ MArr.newArray studentBounds [] :: RandT randomGen (WriterT _ (ST s)) (STArray s (StudentIndex, CloneIndex) [course])
      forM_ clonedStudents $ \(st, cn) ->
        lift . lift . MArr.writeArray stPrefs (st ^. contStudents, cn) $ studentPrefs cstb (st, cn)

      let
        propose :: StateT (Set (student, CloneIndex)) (WriterT _ (ST s)) ()
        propose = (=<< get) . mapM_ $ \(st, cn) -> do
          lift . tell . pure . MatchingConsider st $ fromIntegral cn
          let markDone = modify' $ Set.delete (st, cn)

          options <- lift . lift $ MArr.readArray stPrefs (st ^. contStudents, cn)
          case options of
            []     -> markDone
            c : cs -> do
              lift . lift $ MArr.writeArray stPrefs (st ^. contStudents, cn) cs
              cState <- lift . lift $ MArr.readArray courses' (c ^. contCourses)
              case cState of
                Left pSet
                  | none (\(st', _) -> st == st') pSet -> do
                      lift . tell . pure $ MatchingApply st (fromIntegral cn) c
                      lift . lift . MArr.writeArray courses' (c ^. contCourses) $!! Left (Set.insert (st, cn) pSet)
                      markDone
                Right spots
                  | none (\(st', _) -> st == st') spots -> do
                      courseMatchings <- lift . lift $ MArr.getAssocs courses'
                      let
                        matchingCourse s cn' = listToMaybe $ do
                          (review contCourses -> course, students) <- courseMatchings
                          student <- case students of
                            Left  pSet   -> toList pSet
                            Right spots' -> toList spots'
                          guard $ (s, cn') == student
                          return course

                      let capacity = maybe (error "course without capacity treated as one") fromIntegral . fromMaybe (error "course not found in capacities") $ capacities Map.!? c
                          (worseSpots, betterSpots) = Seq.spanr isWorseSpot spots
                          isWorseSpot existing = case (comparing $ fromMaybe (error "(st, c) not in preferences") . courseRating c &&& stb) existing (st, cn) of
                            EQ -> error "Two student-clones compared equal in the face of stb"
                            GT -> False
                            LT -> True
                          (newSpots, lostSpots) = force . Seq.splitAt capacity $ betterSpots <> Seq.singleton (st, cn) <> worseSpots

                          isUnstableWith :: CloneIndex -> (student, CloneIndex) -> Bool
                          isUnstableWith cn' (stO, cnO) = Just True == (do
                            c' <- matchingCourse st cn'
                            rMe <- courseRating c' (st, cn')
                            rOther <- courseRating c' (stO, cnO)
                            return $ LT == compare (rMe, stb (st, cn')) (rOther, stb (stO, cnO)))

                      if | any (uncurry isUnstableWith) $ (,) <$> [0,1..pred cn] <*> toList lostSpots
                           -> lift . tell . pure $ MatchingNoApplyCloneInstability st (fromIntegral cn) c
                         | Seq.length betterSpots >= capacity
                           -> return ()
                         | otherwise
                           -> do
                             lift . tell . pure $ MatchingApply st (fromIntegral cn) c
                             lift . lift . MArr.writeArray courses' (c ^. contCourses) $ Right newSpots
                             forM_ lostSpots $ \(st', cn') -> do
                               lift . tell . pure $ MatchingLostSpot st' (fromIntegral cn') c
                               modify' $ Set.insert (st', cn')
                             markDone
                _other -> return ()

        proposeLoop = do
          propose
          done <- gets Set.null
          unless done
            proposeLoop

      lift $ evalStateT proposeLoop clonedStudents


      -- let
      --   pairwiseExchange :: ST s ()
      --   pairwiseExchange = do
      --     let possiblePairs = do
      --           (s:ss) <- tails . sortOn stb $ toList clonedStudents
      --           s' <- ss
      --           return (s, s')
      --         matchingCourse (s, c) = do
      --           courseMatchings <- MArr.getAssocs courses'
      --           return . listToMaybe $ do
      --             (course, students) <- courseMatchings
      --             student <- case students of
      --               Left  pSet  -> toList pSet
      --               Right spots -> toList spots
      --             guard $ (s, c) == student
      --             return course
      --     forM_ possiblePairs $ \((a, cna), (b, cnb)) -> void . runMaybeT $ do
      --       ca <- MaybeT $ matchingCourse (a, cna)
      --       cb <- MaybeT $ matchingCourse (b, cnb)

      --       let rank (s, cn) c = Seq.elemIndexL c $ studentPrefs cstb (s, cn)
      --       caRa <- hoistMaybe $ rank (a, cna) ca 
      --       caRb <- hoistMaybe $ rank (b, cnb) ca 
      --       cbRa <- hoistMaybe $ rank (a, cna) cb
      --       cbRb <- hoistMaybe $ rank (b, cnb) cb 

      --       let currentRanks cop = caRa `cop` cbRb
      --           newRanks cop = cbRa `cop` caRb

      --           swapImproves = or
      --             [ currentRanks (+) > newRanks (+)
      --             ,    currentRanks (+) == newRanks (+)
      --               && currentRanks min > newRanks min
      --             ]
      --       lift . when swapImproves $ do
      --         traceM $ show (a, cna) <> " `swap` " <> show (b, cnb)
      --         let
      --           addCourseUser :: course -> (student, CloneIndex) -> ST s ()
      --           addCourseUser c (st, cn) = do
      --             cState <- MArr.readArray courses' c
      --             case cState of
      --               Left pSet ->
      --                 MArr.writeArray courses' c $!! Left (Set.insert (st, cn) pSet)
      --               Right spots ->
      --                 let (worseSpots, betterSpots) = Seq.spanr isWorseSpot spots
      --                     isWorseSpot existing = case (comparing $ courseRating c &&& stb) existing (st, cn) of
      --                       EQ -> error "Two student-clones compared equal in the face of stb"
      --                       GT -> False
      --                       LT -> True
      --                     newSpots = force $ betterSpots <> Seq.singleton (st, cn) <> worseSpots
      --                  in MArr.writeArray courses' c $ Right newSpots
      --           remCourseUser :: course -> (student, CloneIndex) -> ST s ()
      --           remCourseUser c (st, cn) = do
      --             cState <- MArr.readArray courses' c
      --             case cState of
      --               Left pSet ->
      --                 MArr.writeArray courses' c $!! Left (Set.delete (st, cn) pSet)
      --               Right spots ->
      --                 MArr.writeArray courses' c $!! Right (Seq.filter (/= (st, cn)) spots)

      --         remCourseUser ca (a, cna)
      --         remCourseUser cb (b, cnb)
      --         addCourseUser cb (a, cna)
      --         addCourseUser ca (b, cnb)

      -- lift pairwiseExchange


      courseMatchings <- lift . lift $ MArr.getAssocs courses'
      return . Set.fromList $ do
        (review contCourses -> course, students) <- courseMatchings
        student <- case students of
          Left  pSet  -> view _1 <$> toList pSet
          Right spots -> view _1 <$> toList spots
        return (student, course)

    courseRating :: course -> (student, CloneIndex) -> Maybe courseRatingStudent'
    courseRating c (st, cn) = do
      (_, courseRating') <- preferences Map.!? (st, c)
      return $ centralNudge st (fromIntegral cn) courseRating'

    cloneIndices :: cloneIndex -> cloneCount -> Set CloneIndex
    cloneIndices firstClone clones = Set.fromList $ map fromIntegral [firstClone, pred $ firstClone + fromIntegral clones]

    clonedStudents :: Set (student, CloneIndex)
    clonedStudents = Set.fromDistinctAscList $ do
      (student, (firstClone, clones)) <- Map.toAscList cloneCounts
      clone <- Set.toAscList $ cloneIndices firstClone clones
      return (student, clone)

    contStudents :: Iso' student StudentIndex
    contStudents = iso toInt fromInt
      where
        students' = Map.keys cloneCounts

        toInt   = fromMaybe (error "trying to resolve unknown student") . flip elemIndex students'
        fromInt = (!!) students'

    studentBounds :: ((StudentIndex, CloneIndex), (StudentIndex, CloneIndex))
    studentBounds = ((0, 0), (pred $ Map.size cloneCounts, fromMaybe 0 $ maximumOf (folded . to (uncurry cloneIndices) . folded) cloneCounts))

    courses :: Set course
    courses = Set.fromDistinctAscList . map (view _1) . filter (maybe True (> 0) . view _2) $ Map.toAscList capacities
    courseBounds :: (CourseIndex, CourseIndex)
    courseBounds = (0, pred $ Set.size courses)

    contCourses :: Iso' course CourseIndex
    contCourses = iso toInt fromInt
      where
        courses' = Set.toAscList courses

        toInt   = fromMaybe (error "trying to resolve unknown course") . flip elemIndex courses'
        fromInt = (!!) courses'

    initCourse :: course -> Either (Set (student, CloneIndex)) (Seq (student, CloneIndex))
    initCourse c
      | is _Just . join $ Map.lookup c capacities
      = Right Seq.empty
      | otherwise
      = Left Set.empty

    studentPrefs :: forall a. Ord a => (course -> a) -> (student, CloneIndex) -> [course]
    studentPrefs cstb (st, _) = map (view _1) . sortOn (Down . view _2) . mapMaybe (\c -> (c, ) <$> cPref c) $ Set.toList courses
      where
        cPref :: course -> Maybe (studentRatingCourse, a)
        cPref c = do
          (cPref', _) <- Map.lookup (st, c) preferences
          return (cPref', cstb c)
