-- | Common core functions.
module Sound.Sc3.Common.Base where

import Data.Char {- base -}
import Data.List {- base -}
import Data.Maybe {- base -}
import Data.Ord {- base -}

import qualified Safe {- safe -}

-- * Function

-- | Unary function.
type Fn1 a b = a -> b

-- | Binary function.
type Fn2 a b c = a -> b -> c

-- | Ternary function.
type Fn3 a b c d = a -> b -> c -> d

-- | Quaternary function.
type Fn4 a b c d e = a -> b -> c -> d -> e

-- | 5-parameter function.
type Fn5 a b c d e f = a -> b -> c -> d -> e -> f

-- | 6-parameter function.
type Fn6 a b c d e f g = a -> b -> c -> d -> e -> f -> g

-- | 10-parameter function.
type Fn10 a b c d e f g h i j k = a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k

-- | 11-parameter function.
type Fn11 a b c d e f g h i j k l = a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l

{- | Apply /f/ n times, ie. iterate f x !! n

>>> iter 3 (* 2) 1
8

>>> iterate (* 2) 1 !! 3
8
-}
iter :: Int -> (a -> a) -> a -> a
iter n f x = if n == 0 then x else f (iter (n - 1) f x)

-- * Functor

-- | This is the same function as Control.Monad.void, which however hugs does not know of.
fvoid :: Functor f => f a -> f ()
fvoid = fmap (const ())

-- * Read

-- | Variant of 'reads' requiring exact match.
reads_exact :: Read a => String -> Maybe a
reads_exact s =
  case reads s of
    [(r, "")] -> Just r
    _ -> Nothing

-- * String

{- | Similar to Data.List.Split.splitOn, which however hugs doesn't know of.

>>> string_split_at_char ':' "/usr/local/bin:/usr/bin:/bin"
["/usr/local/bin","/usr/bin","/bin"]

>>> string_split_at_char ':' "/usr/local/bin"
["/usr/local/bin"]
-}
string_split_at_char :: Char -> String -> [String]
string_split_at_char c s =
  case break (== c) s of
    (lhs, []) -> [lhs]
    (lhs, _ : rhs) -> lhs : string_split_at_char c rhs

-- * String / Case

-- | Ci = Case insensitive, Cs = case sensitive, Sci = separator & case insensitive
data Case_Rule = Ci | Cs | Sci deriving (Eq)

string_op :: (String -> String -> t) -> Case_Rule -> String -> String -> t
string_op f cr x y =
  let ci_form = map toLower
      sci_form = filter (`notElem` "-_") . ci_form
  in case cr of
      Ci -> f (ci_form x) (ci_form y)
      Cs -> f x y
      Sci -> f (sci_form x) (sci_form y)

{- | String equality with 'Case_Rule'.

>>> string_eq Ci "sinOsc" "SinOsc"
True

>>> string_eq Sci "sin-osc" "SinOsc"
True
-}
string_eq :: Case_Rule -> String -> String -> Bool
string_eq = string_op (==)

string_cmp :: Case_Rule -> String -> String -> Ordering
string_cmp = string_op compare

-- | 'rlookup_by' of 'string_eq'.
rlookup_str :: Case_Rule -> String -> [(a, String)] -> Maybe a
rlookup_str = rlookup_by . string_eq

{- | 'Enum' parser with 'Case_Rule'.

>>> parse_enum Ci "false" :: Maybe Bool
Just False
-}
parse_enum :: (Show t, Enum t, Bounded t) => Case_Rule -> String -> Maybe t
parse_enum cr nm =
  let u = [minBound .. maxBound]
      t = zip (map show u) u
  in lookup_by (string_eq cr) nm t

-- * List

{- | Left to right composition of a list of functions.

>>> compose_l [(* 2),(+ 1)] 3
7
-}
compose_l :: [t -> t] -> t -> t
compose_l = flip (foldl (\x f -> f x))

{- | Right to left composition of a list of functions.

>>> compose_r [(* 2),(+ 1)] 3
8
-}
compose_r :: [t -> t] -> t -> t
compose_r = flip (foldr ($))

{- | SequenceableCollection.differentiate

> [3,4,1,1].differentiate == [3,1,-3,0]

>>> d_dx [3,4,1,1]
[3,1,-3,0]

>>> d_dx [0,1,3,6]
[0,1,2,3]
-}
d_dx :: (Num a) => [a] -> [a]
d_dx l = zipWith (-) l (0 : l)

{- | Variant that does not prepend zero to input, ie. 'tail' of 'd_dx'.

>> d_dx' [3,4,1,1]
[1,-3,0]

>>> d_dx' [0,1,3,6]
[1,2,3]
-}
d_dx' :: Num n => [n] -> [n]
d_dx' l = zipWith (-) (Safe.tailNote "d_dx'" l) l

{- | SequenceableCollection.integrate

> [3,4,1,1].integrate == [3,7,8,9]

>>> dx_d [3,4,1,1]
[3,7,8,9]

>>> dx_d (d_dx [0,1,3,6])
[0,1,3,6]

>>> dx_d [0.5,0.5]
[0.5,1.0]
-}
dx_d :: Num n => [n] -> [n]
dx_d = scanl1 (+)

{- | Variant pre-prending zero to output.

>>> dx_d' [3,4,1,1]
[0,3,7,8,9]

>>> dx_d' (d_dx' [0,1,3,6])
[0,1,3,6]

>>> dx_d' [0.5,0.5]
[0.0,0.5,1.0]
-}
dx_d' :: Num n => [n] -> [n]
dx_d' = (0 :) . dx_d

-- | 'lookup' with equality function.
lookup_by :: (a -> t -> Bool) -> a -> [(t, b)] -> Maybe b
lookup_by f x = fmap snd . find (f x . fst)

-- | Erroring variant, with message.
lookup_by_note :: String -> (a -> t -> Bool) -> a -> [(t, b)] -> b
lookup_by_note msg f x = fromMaybe (error ("lookup_by: " ++ msg)) . lookup_by f x

-- | Erroring variant.
lookup_by_err :: (a -> t -> Bool) -> a -> [(t, b)] -> b
lookup_by_err = lookup_by_note "error"

-- | Reverse 'lookup' with equality function.
rlookup_by :: (b -> b -> Bool) -> b -> [(a, b)] -> Maybe a
rlookup_by f x = fmap fst . find (f x . snd)

{- | (prev,cur,next) triples.

>>> pcn_triples [1..3]
[(Nothing,1,Just 2),(Just 1,2,Just 3),(Just 2,3,Nothing)]
-}
pcn_triples :: [a] -> [(Maybe a, a, Maybe a)]
pcn_triples =
  let f e l = case l of
        e1 : e2 : l' -> (e, e1, Just e2) : f (Just e1) (e2 : l')
        [e'] -> [(e, e', Nothing)]
        [] -> undefined
  in f Nothing

{- | Separate first list element.

>>> sep_first "astring"
Just ('a',"string")
-}
sep_first :: [t] -> Maybe (t, [t])
sep_first l =
  case l of
    e : l' -> Just (e, l')
    _ -> Nothing

{- | Separate last list element.

>>> sep_last "stringb"
Just ("string",'b')
-}
sep_last :: [t] -> Maybe ([t], t)
sep_last =
  let f (e, l) = (reverse l, e)
  in fmap f . sep_first . reverse

{- | Are lists of equal length?

>>> equal_length_p ["t1","t2"]
True

>>> equal_length_p ["t","t1","t2"]
False
-}
equal_length_p :: [[a]] -> Bool
equal_length_p = (== 1) . length . nub . map length

-- | Histogram
histogram :: Ord a => [a] -> [(a, Int)]
histogram x =
  let g = group (sort x)
  in zip (map (Safe.headNote "histogram") g) (map length g)

-- | !! with localised error message
at_with_error_message :: String -> [t] -> Int -> t
at_with_error_message msg list index =
  if index >= length list
    then error ("!!: index out of range: " ++ msg)
    else list !! index

-- | concat of intersperse.  This is the same function as intercalate, which hugs doesn't know of.
concat_intersperse :: [a] -> [[a]] -> [a]
concat_intersperse x = concat . intersperse x

{- | Similar to Data.List.Split.splitOn, which however hugs doesn't know of.

>>> list_split_at_elem ' ' "a sequence of words"
["a","sequence","of","words"]
-}
list_split_at_elem :: Eq t => t -> [t] -> [[t]]
list_split_at_elem c s =
  case break (== c) s of
    (lhs, []) -> [lhs]
    (lhs, _ : rhs) -> lhs : list_split_at_elem c rhs

{- | Data.List.sortOn, which however hugs does not know of.

>>> sort_on snd [('a',1),('b',0)]
[('b',0),('a',1)]
-}
sort_on :: (Ord b) => (a -> b) -> [a] -> [a]
sort_on = sortBy . comparing

{- | Inserts at the first position where it compares less but not equal to the next element.

>>> import Data.Function
>>> insertBy (compare `on` fst) (3,'x') (zip [1..5] ['a'..])
[(1,'a'),(2,'b'),(3,'x'),(3,'c'),(4,'d'),(5,'e')]

>>> insertBy_post (compare `on` fst) (3,'x') (zip [1..5] ['a'..])
[(1,'a'),(2,'b'),(3,'c'),(3,'x'),(4,'d'),(5,'e')]
-}
insertBy_post :: (a -> a -> Ordering) -> a -> [a] -> [a]
insertBy_post cmp e l =
  case l of
    [] -> [e]
    h : l' -> case cmp e h of
      LT -> e : l
      _ -> h : insertBy_post cmp e l'

-- | 'insertBy_post' using 'compare'.
insert_post :: Ord t => t -> [t] -> [t]
insert_post = insertBy_post compare

{- | Apply /f/ at all but last element, and /g/ at last element.

>>> at_last (* 2) negate [1..4]
[2,4,6,-4]
-}
at_last :: (a -> b) -> (a -> b) -> [a] -> [b]
at_last f g x =
  case x of
    [] -> []
    [i] -> [g i]
    i : x' -> f i : at_last f g x'

-- * Tuples

-- | Zip two 4-tuples.
p4_zip :: (a, b, c, d) -> (e, f, g, h) -> ((a, e), (b, f), (c, g), (d, h))
p4_zip (a, b, c, d) (e, f, g, h) = ((a, e), (b, f), (c, g), (d, h))

-- | Two-tuple.
type T2 a = (a, a)

-- | Three-tuple.
type T3 a = (a, a, a)

-- | Four-tuple.
type T4 a = (a, a, a, a)

-- | t -> (t,t)
dup2 :: t -> T2 t
dup2 t = (t, t)

-- | t -> (t,t,t)
dup3 :: t -> T3 t
dup3 t = (t, t, t)

-- | t -> (t,t,t,t)
dup4 :: t -> T4 t
dup4 t = (t, t, t, t)

-- | 'concatMap' of /f/ at /x/ and /g/ at /y/.
mk_duples :: (a -> c) -> (b -> c) -> [(a, b)] -> [c]
mk_duples a b = concatMap (\(x, y) -> [a x, b y])

-- | Length prefixed list variant of 'mk_duples'.
mk_duples_l :: (Int -> c) -> (a -> c) -> (b -> c) -> [(a, [b])] -> [c]
mk_duples_l i a b = concatMap (\(x, y) -> a x : i (length y) : map b y)

-- | 'concatMap' of /f/ at /x/ and /g/ at /y/ and /h/ at /z/.
mk_triples :: (a -> d) -> (b -> d) -> (c -> d) -> [(a, b, c)] -> [d]
mk_triples a b c = concatMap (\(x, y, z) -> [a x, b y, c z])

-- | [x,y] -> (x,y)
t2_from_list :: [t] -> T2 t
t2_from_list l = case l of [p, q] -> (p, q); _ -> error "t2_from_list"

-- | [x,y,z] -> (x,y,z)
t3_from_list :: [t] -> (t, t, t)
t3_from_list l = case l of [p, q, r] -> (p, q, r); _ -> error "t3_from_list"
