module Neet.Population (
                         Population(..)
                         
                       , PopM
                       , PopContext
                       , runPopM
                         
                       , PopSettings(..)
                       , newPop
                         
                       , trainOnce
                       , trainN
                       ) where
import Neet.Species
import Neet.Genome
import Neet.Parameters
import Data.MultiMap (MultiMap)
import qualified Data.MultiMap as MM
import Data.Map (Map)
import qualified Data.Map as M
import Data.List (foldl', maximumBy)
import Data.Maybe
import Control.Monad.Random
import Control.Monad.Fresh.Class
import Control.Monad.Trans.State
import Control.Applicative
import Control.Monad
import Control.Arrow (first, second)
import Data.Function
newtype SpecId = SpecId Int
               deriving (Show, Eq, Ord)
data Population =
  Population { popSize   :: Int                
             , popSpecs  :: !(Map SpecId Species) 
             , popBScore :: !Double             
             , popBOrg   :: !Genome             
             , popBSpec  :: !SpecId             
             , popCont   :: !PopContext         
             , nextSpec  :: !SpecId             
             , popParams  :: Parameters        
             , popParamsS :: Parameters        
             }
  deriving (Show)
data PopContext =
  PC { nextInno :: InnoId
     , randGen  :: StdGen
     } 
  deriving (Show)
newtype PopM a = PopM (State PopContext a)
            deriving (Functor, Applicative, Monad)
instance MonadRandom PopM where
  getRandom = PopM . state $ \s ->
    let (r, gen) = random (randGen s)
    in (r, s { randGen = gen })
  getRandoms = PopM . state $ \s ->
    let (g1, g2) = split $ randGen s
    in (randoms g1, s { randGen = g2 })
  getRandomR range = PopM . state $ \s ->
    let (r, gen) = randomR range (randGen s)
    in (r, s { randGen = gen })
  getRandomRs range = PopM . state $ \s ->
    let (g1, g2) = split $ randGen s
    in (randomRs range g1, s { randGen = g2 })
instance MonadFresh InnoId PopM where
  fresh = PopM . state $ \s ->
    let inno@(InnoId x) = nextInno s
    in (inno, s { nextInno = InnoId $ x + 1 })
runPopM :: PopM a -> PopContext -> (a, PopContext)
runPopM (PopM ma) = runState ma
data PopSettings =
  PS { psSize    :: Int        
     , psInputs  :: Int        
     , psOutputs :: Int        
     , psParams  :: Parameters 
     , psParamsS :: Parameters 
     } 
  deriving (Show)
newtype SpecM a = SM (State SpecId a)
                deriving (Functor, Applicative, Monad)
instance MonadFresh SpecId SpecM where
  fresh = SM . state $ \s@(SpecId x) -> (s, SpecId $ x + 1)
runSpecM :: SpecM a -> SpecId -> (a, SpecId)
runSpecM (SM ma) = runState ma
data SpecBucket =
  SB SpecId Genome [Genome]
shuttleOrgs :: MonadFresh SpecId m =>
               Parameters -> [SpecBucket] -> [Genome] -> m [SpecBucket]
shuttleOrgs p@Parameters{..} buckets gs = foldM shutOne buckets gs
  where DistParams{..} = distParams
        shutOne :: MonadFresh SpecId m => [SpecBucket] -> Genome -> m [SpecBucket]
        shutOne (SB sId rep gs:bs) g
          | distance p g rep <= delta_t = return $ SB sId rep (g:gs) : bs
          | otherwise = liftM (SB sId rep gs :) $ shutOne bs g
        shutOne [] g = do
          newId <- fresh
          return $ [SB newId g [g]]
zipWithDefaults :: (a -> b -> Maybe c) -> (a -> Maybe c) -> (b -> Maybe c) -> [a] -> [b] -> [c]
zipWithDefaults f da db [] bs = mapMaybe db bs
zipWithDefaults f da db as [] = mapMaybe da as
zipWithDefaults f da db (a:as) (b:bs) =
  case f a b of
   Just res -> res : zipWithDefaults f da db as bs
   Nothing -> zipWithDefaults f da db as bs
speciate :: MonadFresh SpecId m =>
            Parameters -> Map SpecId Species -> [Genome] -> m (Map SpecId Species)
speciate params specs gs = do
  filled <- fill
  let zipped = zipWithDefaults mkSpecies (const Nothing) newSpecies specL filled
  return $ M.fromList zipped
  where oneBucket (k, Species _ (rep:_) _ _) = SB k rep []
        assocs = M.toList specs
        specL = map snd assocs
        buckets = map oneBucket assocs
        fill = shuttleOrgs params buckets gs
        mkSpecies (Species _ _ scr imp) (SB sId _ gs)
          | null gs = Nothing
          | otherwise = Just $ (sId, Species (length gs) gs scr imp)
        newSpecies (SB _ _ []) = Nothing
        newSpecies (SB sId _ (g:gs)) = Just $ (sId, newSpec g gs)
newPop :: Int -> PopSettings -> Population
newPop seed PS{..} = fst $ runPopM generate initCont
  where popSize = psSize
        popBScore = 0
        popBSpec = SpecId 1
        initCont = PC (InnoId $ psInputs * psOutputs + 2) (mkStdGen seed)
        popParams = psParams
        popParamsS = psParamsS
        generateGens = replicateM psSize (fullConn psParams psInputs psOutputs)
        generate = do
          gens <- generateGens
          let (popSpecs, nextSpec) = runSpecM (speciate psParams M.empty gens) (SpecId 1)
              popBOrg = head gens
          popCont <- PopM get
          return Population{..}
data BS = Big | Small
trainOnce :: (Genome -> Double) -> Population -> Population
trainOnce f pop = generated
  where params = popParams pop
        paramsS = popParamsS pop
        chooseParams :: Species -> Parameters
        chooseParams s = if specSize s >= largeSize params then params else paramsS
        
        
        initSpecs = popSpecs pop
        
        fits = M.map (\sp -> (sp, runFitTest f sp)) initSpecs
        
        eugenics :: SpecId -> (Species, (MultiMap Double Genome, SpecScore, Double)) ->
                    Maybe (Species, MultiMap Double Genome, Double)
        eugenics sId (sp, (fitmap, ss, adj))
          | maybe False (lastImprovement nSpec >=) (dropTime params)
            && sId /= popBSpec pop = Nothing
          | otherwise = Just (nSpec, fitmap, adj)
          where nSpec = updateSpec ss sp
        
        masterRace :: Map SpecId (Species, MultiMap Double Genome, Double)
        masterRace = M.mapMaybeWithKey eugenics fits
        
        masterList :: [(SpecId,(Species, MultiMap Double Genome, Double))]
        masterList = M.toList masterRace
        
        idVeryBest :: (SpecId, Species)
        idVeryBest = maximumBy (compare `on` (bestScore . specScore . snd)) $ map clean masterList
          where clean (sId,(sp, _, _)) = (sId,sp)
        veryBest = snd idVeryBest
        bestId = fst idVeryBest
        
        masterSpec :: Map SpecId Species
        masterSpec = M.map (\(s,_,_) -> s) masterRace
        totalFitness = M.foldl' (+) 0 . M.map (\(_,_,x) -> x) $ masterRace
        totalSize = popSize pop
        dubSize = fromIntegral totalSize
        
        candSpecs :: MonadRandom m => [(Parameters, Int, m Genome)]
        candSpecs = zip3 ps realShares pickers
          where initShares = map share masterList
                share (_,(_, _, adj)) = round $ adj / totalFitness * dubSize
                remaining = totalSize  foldl' (+) 0 initShares
                distributeRem n [] = error "Should run out of numbers first"
                distributeRem n l@(x:xs)
                  | n > 0 = x + 1 : distributeRem (n  1) xs
                  | otherwise = l
                realShares = distributeRem remaining initShares
                pickers :: MonadRandom m => [m Genome]
                pickers = map picker masterList
                  where picker (_,(s, mmap, _)) =
                          let numToTake = specSize s `div` 5 + 1
                              desc = M.toDescList $ MM.toMap mmap
                              toPairs (k, vs) = map (\v -> (k,v)) vs
                              culled = take numToTake $ desc >>= toPairs
                          in fromList . map (\(d,g) -> (g, toRational d)) $ culled
                ps = map (\(_,(s,_,_)) -> chooseParams s) masterList
        applyN :: Monad m => Int -> (a -> m a) -> a -> m a
        applyN 0 f x = return x
        applyN n f !x = f x >>= applyN (n  1) f
        
        specGens :: (MonadFresh InnoId m, MonadRandom m) =>
                    (Parameters, Int, m Genome) -> m [Genome]
        specGens (p, n, gen) = liftM snd $ applyN n genOne (M.empty, [])
          where genOne (innos, gs) = do
                  roll <- getRandomR (0,1)
                  if roll <= noCrossover p
                    then do
                    parent <- gen
                    (innos', g) <- mutate p innos parent
                    return (innos', g:gs)
                    else do
                    mom <- gen
                    dad <- gen
                    (innos', g) <- breed p innos mom dad
                    return (innos', g:gs)
        allGens :: (MonadRandom m, MonadFresh InnoId m) => m [Genome]
        allGens = liftM concat $ mapM specGens candSpecs
        genNewSpecies :: (MonadRandom m, MonadFresh InnoId m) => m (Map SpecId Species, SpecId)
        genNewSpecies = do
          gens <- allGens
          return $ runSpecM (speciate params masterSpec gens) (nextSpec pop)
        generated :: Population
        generated = fst $ runPopM generate (popCont pop)
        generate :: PopM Population
        generate = do
          (specs, nextSpec') <- genNewSpecies
          let bScoreNow = (bestScore . specScore) veryBest
              bOrgNow = (bestGen . specScore) veryBest
              bSpecNow = bestId
              (bScore, bOrg, bSpec) =
                if bScoreNow > popBScore pop
                then (bScoreNow, bOrgNow, bSpecNow)
                else (popBScore pop, popBOrg pop, popBSpec pop)
          cont' <- PopM get
          return pop { popSpecs = specs
                     , popBScore = bScore
                     , popBOrg = bOrg
                     , popBSpec = bSpec
                     , popCont = cont'
                     , nextSpec = nextSpec'
                     } 
trainN :: Int -> (Genome -> Double) -> Population -> Population
trainN n f p
  | n <= 0 = p
  | otherwise = applyN n (trainOnce f) p
  where applyN 0 f !x = x
        applyN n f !x = applyN (n  1) f (f x)