content: {-# LANGUAGE FlexibleInstances, ScopedTypeVariables, FlexibleContexts, ExtendedDefaultRules, ExistentialQuantification, CPP #-}
  -- |
  -- Internal representations of the Matplotlib data. These are not API-stable
  -- and may change. You can easily extend the provided bindings without relying
  -- on the internals exposed here but they are provided just in case.
  module Graphics.Matplotlib.Internal where
  import System.IO.Temp
  import System.Process
  import Data.Aeson
  import Control.Monad
  import Control.DeepSeq
  import System.IO
  import qualified Data.ByteString.Lazy as B
  import Data.List
  import Control.Exception
  import qualified Data.Sequence as S
  import Data.Sequence (Seq, (|>), (><))
  import Data.Maybe
  import Data.Monoid
  import GHC.Exts(toList)
  
  -- | A handy miscellaneous function to linearly map over a range of numbers in a given number of steps
  mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b]
  mapLinear f s e n = map (\v -> f $ s + (v * (e - s) / n)) [0..n]
  
  -- $ Basics
  
  -- | The wrapper type for a matplotlib computation.
  data Matplotlib = Matplotlib {
    mpCommands :: Seq MplotCommand   -- ^ Resolved computations that have been transformed to commands
    , mpPendingOption :: Maybe ([Option] -> MplotCommand)   -- ^ A pending computation that is affected by applied options
    , mpRest :: Seq MplotCommand  -- ^ Computations that follow the one that is pending
    }
  
  -- | Monoid instance for Matplotlib type
  instance Monoid Matplotlib where
      mempty  = mp
  #if !MIN_VERSION_base(4,11,0)
      mappend = (%)
  #else
  instance Semigroup Matplotlib where
      (<>) = (%)
  #endif
  
  instance NFData Matplotlib where
      rnf (Matplotlib cs po re) = rnf cs `seq` rnf po `seq` rnf re
  
  -- | A maplotlib command, right now we have a very shallow embedding essentially
  -- dealing in strings containing python code as well as the ability to load
  -- data. The loaded data should be a json object.
  data MplotCommand =
    LoadData B.ByteString
    | forall x. MplotImage x => LoadImage x
    | Exec { es :: String }
  
  instance NFData MplotCommand where
      rnf (LoadData b) = rnf b
      rnf (Exec es)    = rnf es
      -- don't care too much about the LoadImage
  
  -- | Throughout the API we need to accept options in order to expose
  -- matplotlib's many configuration options.
  data Option =
    -- | results in a=b
    K String String
    -- | just inserts the option verbatim as an argument at the end of the function
    | P String
    deriving (Show, Eq, Ord)
  
  -- | Convert an 'MplotCommand' to python code, doesn't do much right now
  toPy :: MplotCommand -> String
  toPy (LoadData _) = error "withMplot needed to load data"
  toPy (LoadImage _) = error "withMplot needed to load images"
  toPy (Exec str)   = str
  
  -- | Resolve the pending command with no options provided.
  resolvePending :: Matplotlib -> Matplotlib
  resolvePending m = m { mpCommands =
                         (maybe (mpCommands m)
                                (\pendingCommand -> (mpCommands m |> pendingCommand []))
                                $ mpPendingOption m) >< mpRest m
                       , mpPendingOption = Nothing
                       , mpRest = S.empty}
  
  -- | The io action is given a list of python commands to execute (note that
  -- these are commands in the sense of lines of python code; each inidivudal line
  -- may not be parseable on its own)
  withMplot :: Matplotlib -> ([String] -> IO a) -> IO a
  withMplot m f = preload cs []
    where
      cs = toList $ mpCommands $ resolvePending m
      preload [] cmds = f $ map toPy $ reverse cmds
      preload ((LoadData obj):l) cmds =
            withSystemTempFile "data.json"
              (\dataFile dataHandle -> do
                  B.hPutStr dataHandle obj
                  hClose dataHandle
                  preload l $ ((map Exec $ pyReadData dataFile) ++ cmds))
      preload ((LoadImage img):l) cmds = do
        withSystemTempFile "data.json" $
          (\dataFile dataHandle -> do
              hClose dataHandle
              obj <- saveHaskellImage img dataFile
              preload l $ ([Exec $ "img = " ++ (loadPythonImage img obj dataFile)] ++ cmds))
      preload (c:l) cmds = preload l (c:cmds)
  
  -- | Create a plot that executes the string as python code
  mplotString :: String -> Matplotlib
  mplotString s = Matplotlib S.empty Nothing (S.singleton $ Exec s)
  
  -- | Create an empty plot. This the beginning of most plotting commands.
  mp :: Matplotlib
  mp = Matplotlib S.empty Nothing S.empty
  
  -- | Load the given data into the python "data" array
  readData :: ToJSON a => a -> Matplotlib
  readData d = Matplotlib (S.singleton $ LoadData $ encode d) Nothing S.empty
  
  -- | Load the given image into python "img" variable
  readImage :: MplotImage i => i -> Matplotlib
  readImage i = Matplotlib (S.singleton $ LoadImage i) Nothing S.empty
  
  infixl 5 %
  -- | Combine two matplotlib commands
  (%) :: Matplotlib -> Matplotlib -> Matplotlib
  a % b | isJust $ mpPendingOption b = b { mpCommands = mpCommands (resolvePending a) >< mpCommands b }
        | otherwise = a { mpRest = mpRest a >< mpCommands b >< mpRest b }
  
  infixl 6 #
  -- | Add Python code to the last matplotlib command
  (#) :: (MplotValue val) => Matplotlib -> val -> Matplotlib
  m # v | S.null $ mpRest m =
          case mpPendingOption m of
            Nothing -> m { mpRest = S.singleton $ Exec $ toPython v }
            (Just f) -> m { mpPendingOption = Just (\o -> Exec $ es (f o) ++ toPython v)}
        | otherwise = m { mpRest = S.adjust (\(Exec s) -> Exec $ s ++ toPython v) (S.length (mpRest m) - 1) (mpRest m) }
  
  -- | A string to be rendered in python as a string. In other words it is
  -- rendered as 'str'.
  data S = S String
    deriving (Show, Eq, Ord)
  
  -- | A string to be rendered in python as a raw string. In other words it is
  -- rendered as r'str'.
  data R = R String
    deriving (Show, Eq, Ord)
  
  -- | A string to be rendered in python as a raw literal/code. In other words it is
  -- inserted directly as is into the code.
  data L = L String
    deriving (Show, Eq, Ord)
  
  -- | Values which can be combined together to form a matplotlib command. These
  -- specify how values are rendered in Python code.
  class MplotValue val where
    -- | Render a value inline in Python code
    toPython :: val -> String
    -- | Render a value as an optional parameter in Python code
    toPythonOpt :: val -> String
    toPythonOpt = toPython
  
  instance MplotValue S where
    toPython (S s) = "'" ++ s ++ "'"
  instance MplotValue R where
    toPython (R s) = "r'" ++ s ++ "'"
  instance MplotValue L where
    toPython (L s) = s
  instance MplotValue String where
    -- | A string is just a literal when used in code
    toPython s = s
    -- | A string is a real quoted python string when used as an option
    toPythonOpt s = toPythonOpt $ S s
  instance MplotValue [String] where
    toPython [] = ""
    toPython (x:xs) = toPython x ++ "," ++ toPython xs
    -- | A list of strings is a list of python strings, not literals
    toPythonOpt s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPythonOpt (str x) ++ "," ++ f xs
  instance MplotValue Double where
    toPython s = show s
  instance MplotValue [Double] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue Integer where
    toPython s = show s
  instance MplotValue [Integer] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue Int where
    toPython s = show s
  instance MplotValue [Int] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue [R] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue [S] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue [L] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue Bool where
    toPython s = show s
  instance (MplotValue x) => MplotValue (x, x) where
    toPython (k, v) = "(" ++ toPython k ++ ", " ++ toPython v ++ ")"
  instance (MplotValue (x, y)) => MplotValue [(x, y)] where
    toPython s = "[" ++ f s ++ "]"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  instance MplotValue x => MplotValue (Maybe x) where
    toPython Nothing  = "None"
    toPython (Just x) = toPython x
  instance MplotValue [[Double]] where
    toPython s = "np.asarray([" ++ f s ++ "])"
      where f [] = ""
            f (x:xs) = toPython x ++ "," ++ f xs
  
  default (Integer, Int, Double)
  
  -- | The class of Haskell images or references to imagese which can be
  -- transferred to matplotlib.
  class MplotImage a where
    saveHaskellImage :: a -> FilePath -> IO String
    loadPythonImage :: a -> String -> FilePath -> String
  
  -- | An image that is a string is a file path.
  instance MplotImage String where
    saveHaskellImage _ _ = return ""
    loadPythonImage s _ _ = "mpimg.imread('" ++ toPython s ++ "')"
  
  instance ToJSON a => MplotImage [[a]] where
    saveHaskellImage d fp = (B.writeFile fp $ encode d) >> return ""
    loadPythonImage s _ fp = unlines $ pyReadData fp
  
  -- $ Options
  
  -- | Add an option to the last matplotlib command. Commands can have only one option!
  -- optFn :: Matplotlib -> Matplotlib
  optFn :: ([Option] -> String) -> Matplotlib -> Matplotlib
  optFn f l | isJust $ mpPendingOption l = error "Commands can have only open option. TODO Enforce this through the type system or relax it!"
            | otherwise = l' { mpPendingOption = Just (\os -> Exec (sl `combine` f os)) }
    where (l', (Exec sl)) = removeLast l
          removeLast x@(Matplotlib _ Nothing s) = (x { mpRest = sdeleteAt (S.length s - 1) s }
                                                  , fromMaybe (Exec "") (slookup (S.length s - 1) s))
          removeLast _ = error "TODO complex options"
          -- TODO When containers is >0.5.8 replace these
          slookup i s | i < S.length s = Just $ S.index s i
                      | otherwise      = Nothing
          sdeleteAt i s | i < S.length s = S.take i s >< S.drop (i + 1) s
                        | otherwise      = s
          combine [] r = r
          combine l [] = l
          combine l r | [last l] == "(" && [head r] == "," = l ++ tail r
                      | otherwise = l ++ r
  
  -- | Merge two commands with options between
  options :: Matplotlib -> Matplotlib
  options l = optFn (\o -> renderOptions o) l
  
  infixl 6 ##
  -- | A combinator like '#' that also inserts an option
  (##) :: MplotValue val => Matplotlib -> val -> Matplotlib
  m ## v = options m # v
  
  -- | An internal helper to convert a list of options to the python code that
  -- applies those options in a call.
  renderOptions :: [Option] -> [Char]
  renderOptions [] = ""
  renderOptions xs = f xs
    where  f (P a:l) = "," ++ a ++ f l
           f (K a b:l) = "," ++ a ++  "=" ++ b ++ f l
           f [] = ""
  
  -- | An internal helper that modifies the options of a plot.
  optionFn :: ([Option] -> [Option]) -> Matplotlib -> Matplotlib
  optionFn f m = case mpPendingOption m of
                   (Just cmd) -> m { mpPendingOption = Just (\os -> cmd $ f os) }
                   Nothing -> error "Can't apply an option to a non-option command"
  
  -- | Apply a list of options to a plot resolving any pending options.
  option :: Matplotlib -> [Option] -> Matplotlib
  option m os = resolvePending $ optionFn (\os' -> os ++ os') m
  
  infixl 6 @@
  -- | A combinator for 'option' that applies a list of options to a plot
  (@@) :: Matplotlib -> [Option] -> Matplotlib
  m @@ os = option m os
  
  -- | Bind a list of default options to a plot. Positional options are kept in
  -- order and default that way as well. Keyword arguments are also handled
  bindDefault :: Matplotlib -> [Option] -> Matplotlib
  bindDefault m os = optionFn (bindDefaultFn os) m
  
  -- | Merge two sets of options
  bindDefaultFn :: [Option] -> [Option] -> [Option]
  bindDefaultFn os os' = merge ps' ps ++ (nub $ ks' ++ ks)
             where isK (K _ _) = True
                   isK _ = False
                   isP (P _) = True
                   isP _ = False
                   ps  = filter isP os
                   ps' = filter isP os'
                   ks  = filter isK os
                   ks' = filter isK os'
                   merge l []  = l
                   merge [] l' = l'
                   merge (x:l) (_:l') = (x : merge l l')
  
  -- $ Python operations
  
  -- | Run python given a code string.
  python :: Foldable t => t String -> IO (Either String String)
  python codeStr =
    catch (withSystemTempFile "code.py"
           (\codeFile codeHandle -> do
               forM_ codeStr (hPutStrLn codeHandle)
               hClose codeHandle
               Right <$> readProcess "env" ["python3", codeFile] ""))
           (\e -> return $ Left $ show (e :: IOException))
  
  pyBackend backend = "matplotlib.use('" ++ backend ++ "')"
  
  -- | The standard python includes of every plot
  pyIncludes :: String -> [[Char]]
  pyIncludes backend = ["import matplotlib"
                       ,backend
                       ,"import matplotlib.path as mpath"
                       ,"import matplotlib.patches as mpatches"
                       ,"import matplotlib.pyplot as plot"
                       ,"import matplotlib.cm as cm"
                       ,"import matplotlib.colors as mcolors"
                       ,"import matplotlib.collections as mcollections"
                       ,"import matplotlib.ticker as mticker"
                       ,"import matplotlib.image as mpimg"
                       ,"from mpl_toolkits.mplot3d import axes3d"
                       ,"import numpy as np"
                       ,"from scipy import interpolate"
                       ,"import os"
                       ,"import io"
                       ,"import sys"
                       ,"import json"
                       ,"import random, datetime"
                       ,"from matplotlib.dates import DateFormatter, WeekdayLocator"
                       -- We set this rcParams due to:
                       --     bivariateNormal:    /run/user/1000/code12548-89.py:30: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
                       --   plot.sci(ax.pcolor(np.array(data[0]),np.array(data[1]),np.array(data[2]),cmap=r'PuBu_r'))
                       -- /run/user/1000/code12548-89.py:36: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
                       --   plot.sci(ax.pcolor(np.array(data[0]),np.array(data[1]),np.array(data[2]),norm=mcolors.LogNorm(vmin=1.964128034639681e-6, vmax=7.963602137747198),cmap=r'PuBu_r'))
                       ,"plot.rcParams['pcolor.shading'] ='auto'"
                       ,"fig = plot.gcf()"
                       ,"axes = [plot.gca()]"
                       ,"ax = axes[0]"]
  
  -- | These will be Python strings and slashes would cause unwanted control characters.
  escapeSlashes ('\\':cs) = '\\':'\\':escapeSlashes cs
  escapeSlashes (c:cs) = c:escapeSlashes cs
  escapeSlashes [] = []
  
  -- | The python command that reads external data into the python data array
  pyReadData :: [Char] -> [[Char]]
  pyReadData filename = ["data = json.loads(open('" ++ escapeSlashes filename ++ "').read())"]
  
  -- | The python command that reads an image into the img variable
  pyReadImage :: [Char] -> [[Char]]
  pyReadImage filename = ["img = mpimg.imread('" ++ escapeSlashes filename ++ "')"]
  
  -- | Detach python so we don't block (TODO This isn't working reliably)
  pyDetach :: [[Char]]
  pyDetach = ["pid = os.fork()"
             ,"if(pid != 0):"
             ,"  exit(0)"]
  
  -- | Python code to show a plot
  pyOnscreen :: [[Char]]
  pyOnscreen = ["plot.draw()"
               ,"plot.show()"]
  
  -- | Python code that saves a figure
  pyFigure :: [Char] -> [[Char]]
  pyFigure output = ["plot.savefig('" ++ escapeSlashes output ++ "')"]
  
  -- | Python code that returns SVG for a figure
  pySVG :: [[Char]]
  pySVG =
    ["i = io.StringIO()"
    ,"plot.savefig(i, format='svg')"
    ,"print(i.getvalue())"]
  
  -- | Create a positional option
  o1 x = P $ toPythonOpt x
  
  -- | Create a keyword option
  o2 x = K x . toPythonOpt
  
  -- | Create a string that will be rendered as a python string
  str = S
  
  -- | Create a string that will be rendered as a raw python string
  raw = R
  
  -- | Create a literal that will inserted into the python code directly
  lit = L
  
  -- | Update axes. Should be called any time the state is changed.
  updateAxes = mp # "axes = plot.gcf().get_axes()"
  
  -- | Update the figure and the axes. Should be called any time the state is changed.
  updateFigure = mp # "fig = plot.gcf()"
    % mp # "axes = plot.gcf().get_axes()"
    % mp # "ax = axes[0] if len(axes) > 0 else None"
  
  -- | Smallest element of a list of lists
  minimum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a
  minimum2 l = minimum $ minimum l
  
  -- | Largest element of a list of lists
  maximum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a
  maximum2 l = maximum $ maximum l
  
  