{-# LANGUAGE CPP #-}

module Data.Text.Chart
    ( plot
    , plotWith
    , options
    , height
    ) where

import Control.Monad (forM_)
import Data.Array.IO (newArray, IOArray, getElems, writeArray)
import Data.Char     (isSpace)
import Data.List     (unfoldr, dropWhileEnd)
import Text.Printf   (printf)

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif

data Options =
  Options { height :: Int }

-- default options
options :: Options
options =
  Options { height = 14 }

newArray2D :: Integer -> Integer -> IO (IOArray (Integer, Integer) String)
newArray2D dimX dimY = newArray ((0,0), (dimX, dimY)) " "

splitEvery :: Int -> [a] -> [[a]]
splitEvery n = takeWhile (not . null) . unfoldr (Just . splitAt n)

plot :: [Integer] -> IO ()
plot = plotWith options

pad :: Integral a => [a] -> Int
pad series =
  let floats = fromIntegral <$> series
      toStr :: [Float] -> [String]
      toStr = fmap (printf "%0.2f")
  in  maximum $ length <$> toStr floats

plotWith :: Options -> [Integer] -> IO ()
plotWith options series = do

    -- variables and functions
    let min' = minimum series
    let max' = maximum series
    let range = abs $ max' - min'
    let offset' = 3
    let height' = height options
    let ratio = fromIntegral height' / fromIntegral range :: Float
    let min2 = fromIntegral min' * ratio
    let max2 = fromIntegral max' * ratio
    let rows = round $ abs $ max2 - min2
    let width = length series + 3
    let pad' = pad series

    -- array creation
    arr <- newArray2D rows (toInteger width)
    let write arr [x] [y] = writeArray arr (x, y)
    let result = write arr

    -- axis and labels
    forM_ [min2..max2] $ \y -> do
            let label = fromInteger max' - (y - min2)
                      * fromInteger range / fromIntegral rows
            result [round(y - min2)] [maximum [offset' - 5, 0]] $
                   printf ("%"++ show pad' ++".2f") label
            result [round(y - min2)] [offset' - 1] $
                   if y == 0 then "┼" else "┤"

    -- initial value
    let first = fromInteger (head series) * ratio - min2
    result [round(fromInteger rows - first)] [offset' - 1] "┼"

    -- plot the line
    forM_ [0..(length series - 2)] $ \x' -> do
        let x = toInteger x'
        let offset'' = x + offset'
        let y0 = round (fromInteger (series !! (x' + 0)) * ratio) - round min2
        let y1 = round (fromInteger (series !! (x' + 1)) * ratio) - round min2
        if y0 == y1 then
            result [rows - y0] [offset''] "─"
        else do
            result [rows - y1] [offset''] $
                   if y0 > y1 then "╰" else "╭"
            result [rows - y0] [offset''] $
                   if y0 > y1 then "╮" else "╯"
            let start = minimum [y0, y1] + 1
            let end = maximum [y0, y1]

            forM_ [start..(end - 1)] $ \y ->
                result [rows - y] [offset''] "│"

    -- print the results
    elements <- getElems arr
    let result = splitEvery (width + 1) elements
    forM_ result $ putStrLn . dropWhileEnd isSpace . concat