-- | The simplest, stupidest possible simplex algorithm.
-- The idea here is to be slow, but "obviously correct" so other algorithms
-- can be verified against it.
--
-- That's the plan, at least. For now this is just a first cut of trying to implement simplex.
--
module Numeric.Limp.Solve.Simplex.Maps
    where
import Numeric.Limp.Rep

import Numeric.Limp.Solve.Simplex.StandardForm

import Control.Arrow
import qualified Data.Map as M
import Data.Function (on)
import Data.List (minimumBy, sortBy)


-- | Result of a single pivot attempt
data IterateResult z r c
    -- | Maximum reached!
    = Done
    -- | Pivot was made
    | Progress (Standard z r c)
    -- | No progress can be made: unbounded along the objective
    | Stuck

deriving instance (Show z, Show r, Show (R c)) => Show (IterateResult z r c)


-- | Try to find a pivot and then perform it.
-- We're assuming, at this stage, that the existing solution is feasible.
simplex1 :: (Ord z, Ord r, Rep c)
        => Standard z r c -> IterateResult z r c
simplex1 s
 -- Check if there are any positive columns in the objective:
 = case pivotCols of
    -- if there are none, we are already at the maximum
    []
     -> Done
    -- there are some; try to find the first pivot row that works
    _
     -> go pivotCols
 where

  -- Check if there's any row worth pivoting on for this column.
  -- We're trying to see if we can increase the value of this
  -- column's variable from zero.
  go ((pc,_):pcs)
   = case pivotRowForCol s pc of
       Nothing -> go pcs
       Just pr
        -> Progress
        -- Perform the pivot.
        -- This moves the variable pr out of the basis, and pc into the basis.
         $ pivot s (pr,pc)

  -- We've tried all the pivot columns and failed.
  -- This means there's no edge we can take to increase our objective,
  -- so it must be unbounded.
  go []
   = Stuck


  -- We want to find some positive column from the objective.
  -- In fact, find all of them and order descending.
  pivotCols
   = let ls  = M.toList $ fst $ _objective s
         kvs = sortBy (compare `on` (negate . snd)) ls
     in  filter ((>0) . snd) kvs


-- | Find pivot row for given column.
-- We're trying to find a way to increase the value of
-- column from zero, and the returned row will be decreased to zero.
-- Since all variables are >= 0, we cannot return a row that would set the column to negative.
pivotRowForCol :: (Ord z, Ord r, Rep c)
        => Standard z r c
        -> StandardVar z r
        -> Maybe (StandardVar z r)
pivotRowForCol s col
 = fmap   fst
 $ minBy' (compare `on` snd)
 $ concatMap (\(n,r)
           -> let rv = lookupRow r col
                  o  = objOfRow  r
              in if    rv > 0
                 then [(n, o / rv)]
                 else [])
 $ M.toList
 $ _constraints s

-- | Find minimum, or nothing if empty
minBy' :: (a -> a -> Ordering) -> [a] -> Maybe a
minBy' _ []
 = Nothing
minBy' f ls
 = Just $ minimumBy f ls


-- | Perform pivot for given row and column.
-- We normalise row so that row.column = 1
--
-- > norm = row / row[column]
--
-- Then, for all other rows including the objective,
-- we want to make sure its column entry is zero:
--
-- > row' = row - row[column]*norm
--
-- In the end, this means "column" will be an identity column, or a basis column.
--
pivot   :: (Ord z, Ord r, Rep c)
        => Standard z r c
        -> (StandardVar z r, StandardVar z r)
        -> Standard z r c
pivot s (pr,pc)
 = let norm = normaliseRow
       -- All other rows
       rest = filter ((/=pr) . fst) $ M.toList $ _constraints s
   in Standard
    { _constraints = M.fromList ((pc, norm) : map (id *** fixup norm) rest)
    , _objective   = fixup norm $ _objective s
    , _substs      = _substs s }
 where
  -- norm = row / row[column]
  normaliseRow
   | Just row@(rm, ro) <- M.lookup pr $ _constraints s
   = let c' = lookupRow row pc
     in  (M.map (/c') rm, ro / c')

   -- Pivot would not be chosen if row doesn't exist..
   | otherwise
   = (M.empty, 0)

  -- row' = row - row[column]*norm
  fixup (nm,no) row@(rm,ro)
   = let co = lookupRow row pc
     in  {- row' = row - co*norm -}
         ( M.unionWith (+) rm (M.map ((-co)*) nm)
         , ro - co * no )


-- | Single phase of simplex.
-- Keep repeating until no progress can be made.
single_simplex :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Maybe (Standard z r c)
single_simplex s
 = case simplex1 s of
    Done        -> Just     s
    Progress s' -> single_simplex  s'
    Stuck       -> Nothing


-- | Two phase:
--  first, find a satisfying solution.
--  then, solve simplex as normal.
simplex
        :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Maybe (Standard z r c)
simplex s
 =   find_initial_sat s
 >>= single_simplex

-- | Find a satisfying solution.
--   if there are any rows with negative values, this means their basic values are negative
--   (which is not satisfying the x >= 0 constraint)
--   these negative-valued rows must be pivoted around using modified pivot criteria
find_initial_sat
        :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Maybe (Standard z r c)
find_initial_sat s
 = case negative_val_rows of
    []      -> Just s
    rs      -> go rs
 where
  -- Find all rows with negative values
  -- because their current value is not feasible
  negative_val_rows
   = filter ((<0) . objOfRow . snd)
   $ M.toList
   $ _constraints s

  -- Find largest negative (closest to zero) to pivot on:
  -- pivoting on a negative will negate the value, setting it to positive
  min_of_row (_,(rm,_))
   = minBy' (compare `on` (negate . snd))
   $ filter ((<0) . snd)
   $ M.toList rm


  -- There is no feasible solution
  go []
   = Nothing

  -- Try pivoting on the rows 
  go (r:rs)
   | Just (pc,_) <- min_of_row r
   , Just  pr    <- pivotRowForNegatives pc
   = simplex
   $ pivot s (pr, pc)

   | otherwise
   = go rs

  -- opposite of pivotRowForCol...
  pivotRowForNegatives col
   = fmap   fst
   $ minBy' (compare `on` (negate . snd))
   $ concatMap (\(n,r)
             -> let rv = lookupRow r col
                    o  = objOfRow  r
                in if    rv < 0
                   then [(n, o / rv)]
                   else [])
   $ M.toList
   $ _constraints s


  

-- Get map of each constraint's value
assignmentAll :: (Rep c)
        => Standard z r c
        -> (M.Map (StandardVar z r) (R c), R c)
assignmentAll s
 = ( M.map    val (_constraints s)
   , objOfRow     (_objective  s))
 where
  val (_, v)
   = v

-- Perform reverse substitution on constraint values
-- to get original values (see StandardForm)
assignment
        :: (Ord z, Ord r, Rep c)
        => Standard z r c
        -> (Assignment () (Either z r) c, R c)
assignment s
 = ( Assignment M.empty $ M.union vs' rs'
   , o )
 where
  (vs, o) = assignmentAll s

  vs'     = M.fromList
          $ concatMap only_svs
          $ M.toList vs

  rs'     = M.map eval $ _substs s

  eval (lin,co)
          = M.foldr (+) co
          $ M.mapWithKey (\k r -> r * (maybe 0 id $ M.lookup k vs))
          $ lin

  only_svs (SV v, val)
   = [(v, val)]
  only_svs _
   = []



-- Junk ---------------

-- | Minimise whatever variables are 'basic' in given standard
-- input must not already have an objective row "SVO",
-- because the existing objective is added as a new row with that name
minimise_basics
        :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Standard z r c
minimise_basics s
 = s
 { _objective   = (M.map (const (1)) $ _constraints s, 0)
 , _constraints = M.insert SVO (_objective s) (_constraints s)
 }

-- | Find the basic variables and "price them out" of the objective function,
-- by subtracting multiples of the basic row from objective
pricing_out 
        :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Standard z r c
pricing_out s
 = s
 { _objective = M.foldrWithKey  go
                    (_objective   s)
                    (_constraints s)
 }
 where
  go v row@(rm,ro) obj@(om,oo)
   | coeff <- lookupRow obj v
   , coeff /= 0
   , rowv  <- lookupRow row v
   , mul   <- -(coeff / rowv)
   = -- rowv = 1
     -- obj' = obj - (coeff/rowv)*row
     ( M.unionWith (+) om (M.map (mul*) rm)
     , oo + mul*ro )
   | otherwise
   = obj

-- | Pull the previously-hidden objective out of constraints, and use it
drop_fake_objective
        :: (Ord z, Ord r, Rep c)
        => Standard z r c -> Standard z r c
drop_fake_objective s
 | cs     <- _constraints s
 , Just o <- M.lookup SVO cs
 = s
 { _objective   = o
 , _constraints = M.delete SVO cs }

 | otherwise
 = s



