{-# LANGUAGE RecordWildCards #-}

module Data.Analysis.Fulcrum.Analysis
  ( module Data.Analysis.Fulcrum.Abstract
  -- * Plans
  , Plan(..)
  , runPlanToMap
  , defaultPlan

  -- * Helper Types
  , Merge
  , Select
  , AbstractTuple
  , select
  , slName
  , Result

  -- * Utility Functions
  , getUniqVals
  , getAxisValues
  , getLineValues
  , validRowValue

  -- Reduction functions
--  , opBy
--  , divBy
  ) where

import Control.Applicative ((<$>))
import Control.Arrow (second)
import Data.Dynamic
import Data.Maybe (catMaybes)
import Data.Monoid (Monoid(..), (<>))
import Data.List (nub, sort, find)
import Data.Map.Strict (Map, fromList)
import Math.Statistics (average)

import Data.Analysis.Fulcrum.Abstract

-- | The value extracted by a 'Select' may be the result of
--   several combined 'Select'ors, and so is represented
--   as a list of 'Abstract' values.
type AbstractTuple = [Abstract]

-- | A 'Merge' function is anything that takes several
--   rows and merges them to a single row.
type Merge r = [r] -> r

-- | A 'Select' encapsulates the ability to extracts a list of abstract
--   values as well as the human-readable name for the field it is
--   extracting. The 'Monoid' instance for 'Select' allows us to take
--   two possibly heterogeneous 'Getter's and combine them.
data Select r = Select
  { _slLens :: r -> AbstractTuple
  , _slName :: String
  }

-- | The human-readable name of a 'Select'.
slName :: Select r -> String
slName = _slName

-- | Turn a 'Getter' for a specific type as well as its human-readable
--   equivalent into a 'Getter' of a generic value.
select :: (Typeable a, Ord a, Show a) => (r -> a) -> String -> Select r
select l s = Select (go . l) s
  where go x = [makeAbs x]

-- | A convenience function to combine strings with commas
comma :: String -> String -> String
comma x "" = x
comma "" y = y
comma x y  = x <> "," <> y

instance Monoid (Select r) where
  mappend (Select l ln) (Select r rn) =
    Select (\ x -> l x <> r x) (comma ln rn)
  mempty = Select (const mempty) ""

-- | A convenience function to run a selector over data
extract :: Select r -> r -> AbstractTuple
extract (Select l _) r = (l r)

-- | A plan describes how to go from a dataset of type @[row]@
--   and arrive at a @Result@ map
data Plan row focus = Plan
  { planFocus   :: row -> focus     -- ^ The final dependent variable
  , planFCName  :: String           -- ^ The name of the dependent variable
  , planName    :: String           -- ^ The name of the plan
  , planFilters :: row -> Bool      -- ^ select which rows to keep
  , planMaps    :: row -> row       -- ^ transform them somehow
  , planAxis    :: Select row       -- ^ select an independent variable
  , planLines   :: Select row       -- ^ select sets of interesting data
  , planMerge   :: Merge focus      -- ^ join the resulting rows together
  , planLinAxis :: Bool             -- ^ true if the axis is linear
  }

-- | The default plan has sensible defaults where possible, and
--   is filled in with undefined functions
defaultPlan :: (Floating b) => Plan a b
defaultPlan = Plan
  { planFocus   = error "undefined focus"
  , planFCName  = "undefined"
  , planName    = "default plan"
  , planFilters = const True
  , planMaps    = id
  , planAxis    = mempty
  , planLines   = mempty
  , planMerge   = average
  , planLinAxis = True
  }

-- | The result of a plan is always going to be a map from
--   (abstract but comparable) keys to some kind of result
--   value.
type Result r = Map (AbstractTuple, AbstractTuple) r

-- | Given a selector and a list of rows, groups into sub-lists that
--   share a common value for the selector.
groupOn :: (Select r) -> [r] -> [[r]]
groupOn l rs = [ filter (go v) rs |  v <- getUniqVals l rs ]
  where go v r = extract l r == v

-- | Extract every unique value that appears in the data
getUniqVals :: (Select r) -> [r] -> [AbstractTuple]
getUniqVals l rs = sort (nub (map (extract l) rs))

-- | Merge the focused value using the merge function
combine :: (r -> f) -> Merge f -> [r] -> f
combine focus merge = merge . map focus

-- | Group into a map based on the axis values and lines
group :: Select r -> Select r -> [r] -> [((AbstractTuple, AbstractTuple), [r])]
group l a rs =
  [ ((extract a (head r), extract l (head r)), r)
  | r <- groupOn (l <> a) rs
  ]

-- | Execute a given 'Plan' on a set of data and return the 'Result' set.
runPlanToMap :: Plan r f -> [r] -> Result f
runPlanToMap p@(Plan { .. }) rawData =
  fromList $ map (second (combine planFocus planMerge))
           $ group planLines planAxis
           $ map planMaps
           $ filter planFilters
           $ rawData

-- | A convenience function for writing maps over rows
opBy :: (a -> a -> a) -> (a -> r -> r) -> (r -> a) -> (r -> a) -> r -> r
opBy op set l r x = set ((l x) `op` (r x)) x

-- | A specialized convenience function for normalizing a given value
--   by another by division
-- divBy :: Lens' r Double -> (r -> Double) -> r -> r
-- divBy l = opBy (/) l (^.l)

-- | A specialized convenience function for normalizing a given value
--   by another using an nth root
-- rootBy :: Lens' r Double -> (r -> Double) -> r -> r
-- rootBy l = undefined

-- | Get the unique points to graph along the X axis
getAxisValues :: Plan r f -> [r] -> [AbstractTuple]
getAxisValues (Plan { .. }) rows =
  getUniqVals planAxis (filter planFilters rows)

-- | Get the human-readable name of the X axis variable
getAxisName :: Plan r f -> String
getAxisName = slName . planAxis

-- | Get the unique field values which determine the set of lines to produce
getLineValues :: Plan r f -> [r] -> [AbstractTuple]
getLineValues (Plan { .. }) rows =
  getUniqVals planLines (filter planFilters rows)

-- | Get the human-readable name of the lines
getLineName :: Plan r f -> String
getLineName = slName . planLines

-- | Determine whether a given (abstract) value appears in the data set
validRowValue :: Select r -> [r] -> AbstractTuple -> Bool
validRowValue l rs a = any (== a) (getUniqVals l rs)
