{-| GCode pretty-printing functions

Utilities for manipulating and filtering 'GCode'

-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE BangPatterns #-}
module Data.GCode.Utils where

import Debug.Trace
import Data.Maybe
import Data.Monoid

import Data.GCode.Types
import qualified Data.Map.Strict as M

import Control.Monad.State.Strict
import Control.Applicative

-- |True if 'Code' is a G-code
isG :: Code -> Bool
isG Code{codeCls=(Just G), ..} = True
isG _ = False

-- |True if 'Code' is a M-code
isM :: Code -> Bool
isM Code{codeCls=(Just M), ..} = True
isM _ = False

-- |True if 'Code' is a G{N} code
isGN :: Int -> Code -> Bool
isGN n Code{codeCls=(Just G), codeNum=(Just x), ..} = x == n
isGN _ _ = False

-- |True if 'Code' is a G{N}.{sub} code
isGNs n sub Code{codeCls=(Just G), codeNum=(Just x), codeSub=(Just sx), ..} = x == n && sx == sub
isGNs _ _ _ = False

-- |True if 'Code' is a M{N} code
isMN :: Int -> Code -> Bool
isMN n Code{codeCls=(Just M), codeNum=(Just x), ..} = x == n
isMN _ _ = False

-- |True if 'Code' is a M{N}.{sub} code
isMNs n sub Code{codeCls=(Just M), codeNum=(Just x), codeSub=(Just sx), ..} = x == n && sx == sub
isMNs _ _ _ = False

-- |True if 'Code' is a G0 code
isG0 :: Code -> Bool
isG0 = isGN 0

-- |True if 'Code' is a G0 (rapid move) code, alias to 'isG0'
isRapid :: Code -> Bool
isRapid = isG0

-- |True if 'Code' is a G1 code
isG1 :: Code -> Bool
isG1 = isGN 1

-- |True if 'Code' is a G1 (move) code, alias to 'isG1'
isMove :: Code -> Bool
isMove = isG1

-- |True if 'Code' is a G2 code
isG2 :: Code -> Bool
isG2 = isGN 2

-- |True if 'Code' is a G2 (clockwise circular move) code, alias to 'isG2'
isArcCW :: Code -> Bool
isArcCW = isG2

-- |True if 'Code' is a G3 code
isG3 :: Code -> Bool
isG3 = isGN 3

-- |True if 'Code' is a G3 (counter-clockwise circular move) code, alias to 'isG3'
isArcCCW :: Code -> Bool
isArcCCW = isG3

-- |True if 'Code' is a G4 code
isG4 :: Code -> Bool
isG4 = isGN 4

-- |True if 'Code' is a G4 (dwell) code, alias to 'isG4'
isDwell :: Code -> Bool
isDwell = isG4

-- |True if 'Code' is a G5 code
isG5 :: Code -> Bool
isG5 = isGN 5

-- |True if 'Code' is a G5 (cubic spline) code, alias to 'isG5'
isCubicSpline :: Code -> Bool
isCubicSpline = isG5

-- |True if 'Code' is a G5.1 code
isG5s1 :: Code -> Bool
isG5s1 = isGNs 5 1

-- |True if 'Code' is a G5.1 (quadratic spline) code, alias to 'isG5s1'
isQuadSpline :: Code -> Bool
isQuadSpline = isG5s1

-- |True if 'Code' is a G5.2 code
isG5s2 :: Code -> Bool
isG5s2 = isGNs 5 2

-- |True if 'Code' is a G5.2 (NURBS) code, alias to 'isG5s2'
isNURBS :: Code -> Bool
isNURBS = isG5s2

-- |True if 'Code' is a G17 (select XYZ plane) code
isXYZplane :: Code -> Bool
isXYZplane = isGN 17

-- |True if 'Code' is a G18 (select XZY plane) code
isXZYplane :: Code -> Bool
isXZYplane = isGN 18

-- |True if 'Code' is a G19 (select YZX plane) code
isYZXplane :: Code -> Bool
isYZXplane = isGN 19

groupPlane = [ isXYZplane, isXZYplane, isYZXplane ]

-- |True if 'Code' is a G20 (inch mode) code
isInch :: Code -> Bool
isInch = isGN 20

-- |True if 'Code' is a G21 (millimeter mode) code
isMM :: Code -> Bool
isMM = isGN 21

groupUnits = [ isInch, isMM ]

-- |True if 'Code' is a G33 code
isG33 :: Code -> Bool
isG33 = isGN 33

-- |True if 'Code' is a G33 (spindle synchronized motion) code, alias to 'isG33'
isSpindleSync :: Code -> Bool
isSpindleSync = isG33

-- |True if 'Code' is a G33.1 code
isG33s1 :: Code -> Bool
isG33s1 = isGNs 33 1

-- |True if 'Code' is a G33.1 (rigit tapping) code, alias to 'isG33s1'
isRigidTap :: Code -> Bool
isRigidTap = isG33s1

-- |True if 'Code' is a G38 code
isG38 :: Code -> Bool
isG38 = isGN 38

-- |True if 'Code' is a G38 (probe) code, alias to 'isG38'
isProbe :: Code -> Bool
isProbe = isG38

groupMotion = [isMove, isRapid, isArcCW, isArcCCW,
  isCubicSpline, isQuadSpline, isNURBS, isProbe, isSpindleSync, isRigidTap]

-- |True if 'Code' is a G73 (drilling cycle, chip breaking) code
isDrillingCycleCB :: Code -> Bool
isDrillingCycleCB = isGN 73

-- |True if 'Code' is a G76 (threading cycle) code
isThreadingCycle :: Code -> Bool
isThreadingCycle = isGN 76

-- |True if 'Code' is a G80 (cancel drilling cycle) code
isDrillingCycleCancel :: Code -> Bool
isDrillingCycleCancel = isGN 80

-- |True if 'Code' is a G81 (drilling cycle) code
isDrillingCycle :: Code -> Bool
isDrillingCycle = isGN 81

-- |True if 'Code' is a G82 (drilling cycle, dwell) code
isDrillingCycleDwell :: Code -> Bool
isDrillingCycleDwell = isGN 82

-- |True if 'Code' is a G83 (drilling cycle, pecky) code
isDrillingCyclePeck :: Code -> Bool
isDrillingCyclePeck = isGN 83

-- |True if 'Code' is a G85 (boring cycle, feed out) code
isBoringCycle :: Code -> Bool
isBoringCycle = isGN 85

-- |True if 'Code' is a G89 (boring cycle, dwell, feed out) code
isBoringCycleDwell :: Code -> Bool
isBoringCycleDwell = isGN 89

groupCycles = [isDrillingCycle, isDrillingCycleCB, isDrillingCyclePeck,
  isDrillingCycleDwell, isDrillingCycleCancel,
  isThreadingCycle,
  isBoringCycle, isBoringCycleDwell ]

-- |True if 'Code' is a G90 (absolute mode) code
isAbsolute :: Code -> Bool
isAbsolute = isGN 90

-- |True if 'Code' is a G91 (relative mode) code
isRelative :: Code -> Bool
isRelative = isGN 91

-- |True if 'Code' is a G90.1 (absolute arc mode) code
isArcAbsolute :: Code -> Bool
isArcAbsolute = isGNs 90 1

-- |True if 'Code' is a G91.1 (relative arc mode) code
isArcRelative :: Code -> Bool
isArcRelative = isGNs 91 1

-- |True if 'Code' is a G7 (lathe diameter mode) code
isLatheDiameter :: Code -> Bool
isLatheDiameter = isGN 7

-- |True if 'Code' is a G8 (lathe radius mode) code
isLatheRadius :: Code -> Bool
isLatheRadius = isGN 8

groupDistance = [ isAbsolute, isRelative,
  isArcAbsolute, isArcRelative, isLatheDiameter, isLatheRadius ]

--isModal = isModalGMotion || isModalPlane || isModalUnits || isModalAbsRel

-- |True if 'Code' is a G93 (inverse time mode) code
isInverseTime :: Code -> Bool
isInverseTime = isGN 93

-- |True if 'Code' is a G94 (units per minute time mode) code
isUnitsPerMinute :: Code -> Bool
isUnitsPerMinute = isGN 94

-- |True if 'Code' is a G95 (units per revolution time mode) code
isUnitsPerRevolution :: Code -> Bool
isUnitsPerRevolution = isGN 95

groupFeedRateMode = [ isInverseTime, isUnitsPerMinute, isUnitsPerRevolution ]

-- |True if 'Code' is a M3 (spindle start clockwise) code
isSpindleCW :: Code -> Bool
isSpindleCW = isMN 3

-- |True if 'Code' is a M4 (spindle start counter-clockwise) code
isSpindleCCW :: Code -> Bool
isSpindleCCW = isMN 4

-- |True if 'Code' is a M5 (spindle stop) code
isSpindleStop :: Code -> Bool
isSpindleStop = isMN 5

groupSpindleControl = [ isSpindleCW, isSpindleCCW, isSpindleStop,
  isMN 19, isGN 96, isGN 97]

-- |True if 'Code' is a M7 (turn mist coolant on) code
isCoolantMist :: Code -> Bool
isCoolantMist = isMN 7

-- |True if 'Code' is a M8 (turn flood coolant on) code
isCoolantFlood :: Code -> Bool
isCoolantFlood = isMN 8

-- |True if 'Code' is a M9 (turn all coolant off) code
isCoolantStop :: Code -> Bool
isCoolantStop = isMN 9

groupCoolantControl = [ isCoolantMist, isCoolantFlood, isCoolantStop ]

-- |True if 'Code' is a G43 (tool length offset) code
isToolLength :: Code -> Bool
isToolLength = isGN 43

-- |True if 'Code' is a G43.1 (dynamic tool length offset) code
isToolLengthDynamic :: Code -> Bool
isToolLengthDynamic = isGNs 43 1

-- |True if 'Code' is a G43.2 (apply additional tool length offset) code
isToolLengthAdd :: Code -> Bool
isToolLengthAdd = isGNs 43 2

-- |True if 'Code' is a G49 (cancel tool length offset) code
isToolLengthCancel :: Code -> Bool
isToolLengthCancel = isGN 49

groupToolLengthOffset = [ isToolLength, isToolLengthDynamic,
  isToolLengthAdd, isToolLengthCancel ]

-- |True if 'Code' is a M0 (pause) code
isPause :: Code -> Bool
isPause = isMN 0

-- |True if 'Code' is a M1 (optional pause) code
isOptionalPause :: Code -> Bool
isOptionalPause = isMN 1

-- |True if 'Code' is a M2 (program end) code
isEnd :: Code -> Bool
isEnd = isMN 2

-- |True if 'Code' is a M30 (exchange pallet shuttles) code
isExchange :: Code -> Bool
isExchange = isMN 30

groupStopping = [ isPause, isOptionalPause, isEnd, isExchange, isMN 60 ]

groups = [ groupMotion, groupCycles, groupDistance, groupFeedRateMode,
  groupSpindleControl, groupCoolantControl, groupStopping, groupUnits,
  groupPlane ]

-- |True if 'Code' has a coordinate in axis 'a'
hasAxis :: AxisDesignator -> Code -> Bool
hasAxis a Code{..} = M.member a codeAxes
hasAxis a _ = False

getAxis :: AxisDesignator -> Code -> Maybe Double
getAxis a Code{..} = M.lookup a codeAxes
getAxis _ _ = Nothing

getAxes :: [AxisDesignator] -> Code -> [Maybe Double]
getAxes as c@Code{..} = map (\a-> getAxis a c) as
getAxes _ _ = []

getAxesToList :: Code -> [(AxisDesignator, Double)]
getAxesToList Code{..} = M.toList codeAxes
getAxesToList _ = []

--filterAxes :: [AxisDesignator] -> Code -> [Double]
--filterAxes ax Code{..} = map (\a -> M.lookup a codeAxes) ax

-- |True if 'Code' contains 'X' axis
hasX :: Code -> Bool
hasX = hasAxis X

-- |True if 'Code' contains 'Y' axis
hasY :: Code -> Bool
hasY = hasAxis Y

-- |True if 'Code' contains 'Z' axis
hasZ :: Code -> Bool
hasZ = hasAxis Z

-- |True if 'Code' contains 'E' axis
hasE :: Code -> Bool
hasE = hasAxis E

-- |True if 'Code' contains parameter with 'ParamDesignator'
hasParam :: ParamDesignator -> Code -> Bool
hasParam p Code{..} = M.member p codeParams
hasParam a _ = False

getParam :: ParamDesignator -> Code -> Maybe Double
getParam p Code{..} = M.lookup p codeParams

-- |True if 'Code' contains feedrate parameter (e.g. G0 F3000)
hasFeedrate :: Code -> Bool
hasFeedrate = hasParam F

-- |Filter G-codes
gcodes :: [Code] -> [Code]
gcodes = filter isG

-- |Filter M-codes
mcodes :: [Code] -> [Code]
mcodes = filter isM

-- |Filter rapid moves
rapids :: [Code] -> [Code]
rapids = filter isRapid

-- |Filter moves
moves :: [Code] -> [Code]
moves  = filter isMove

-- |Replace 'Class' of 'Code' (e.g. for chaning G0 to M0)
replaceClass :: Class -> Code -> Code
replaceClass newclass c = appmod (cls newclass) c

-- |Replace code value of 'Code' (e.g. for chaning G0 to G1)
replaceCode :: Int -> Code -> Code
replaceCode newcode c = appmod (num newcode) c

-- |Replace axis with 'AxisDesignator' in 'Code' returning new 'Code'
replaceAxis :: AxisDesignator -> Double -> Code -> Code
replaceAxis de val c@Code{..} | hasAxis de c = addReplaceAxis de val c
replaceAxis _ _ c = c

modifyAxis :: AxisDesignator -> (Double -> Double) -> Code -> Code
modifyAxis de f c@Code{..} | hasAxis de c = addReplaceAxis de (f $ fromJust $ getAxis de c) c
modifyAxis _ _ c = c

modifyAxes :: [AxisDesignator] -> (Double -> Double) -> Code -> Code
modifyAxes axes f c = foldl (\c1  ax -> modifyAxis ax f c1) c axes

hasXY c = hasAxis X c && hasAxis Y c

modifyXY :: (Double -> Double -> (Double, Double)) -> Code -> Code
modifyXY f c | hasXY c =
  let x = fromJust $ getAxis X c
      y = fromJust $ getAxis Y c
      (nx, ny) = f x y
  in appmod (axis' X nx <> axis' Y ny) c
modifyXY _ c = c

-- |Replace or add axis with 'AxisDesignator' in 'Code' returning new 'Code'
addReplaceAxis :: AxisDesignator -> Double -> Code -> Code
addReplaceAxis de val c@Code{..} = appmod (axes $ newaxes $ codeAxes) c
  where
    newaxes = M.insert de val
addReplaceAxis _ _ x = x

-- |Replace X axis coordnate
replaceX :: Double -> Code -> Code
replaceX = replaceAxis X

-- |Replace Y axis coordinate
replaceY :: Double -> Code -> Code
replaceY = replaceAxis Y

-- |Replace Z axis coordinate
replaceZ :: Double -> Code -> Code
replaceZ = replaceAxis Z

-- |Replace E axis coordinate
replaceE :: Double -> Code -> Code
replaceE = replaceAxis E

-- |Replace or add X axis coordinate
addReplaceX :: Double -> Code -> Code
addReplaceX = addReplaceAxis X

-- |Replace or add Y axis coordinate
addReplaceY :: Double -> Code -> Code
addReplaceY = addReplaceAxis Y

-- |Replace or add Z axis coordinate
addReplaceZ :: Double -> Code -> Code
addReplaceZ = addReplaceAxis Z

-- |Replace or add E axis coordinate
addReplaceE :: Double -> Code -> Code
addReplaceE = addReplaceAxis E


-- |Replace parameter with 'ParamDesignator' in 'Code' returning new 'Code'
replaceParam :: ParamDesignator -> Double -> Code -> Code
replaceParam de val c@Code{..} | hasParam de c = addReplaceParam de val c
replaceParam _ _ c = c

modifyParam :: ParamDesignator -> (Double -> Double) -> Code -> Code
modifyParam de f c@Code{..} | hasParam de c = addReplaceParam de (f $ fromJust $ getParam de c) c
modifyParam _ _ c = c

-- |Replace or add parameter with 'ParamDesignator' in 'Code' returning new 'Code'
addReplaceParam :: ParamDesignator -> Double -> Code -> Code
addReplaceParam de val c@Code{..} = appmod (params $ newparams $ codeParams) c
  where
    newparams = M.insert de val
addReplaceParam _ _ x = x

-- |Replace feedrate (F parameter) in 'Code' returning new 'Code'
replaceFeedrate :: Double -> Code -> Code
replaceFeedrate = replaceParam F

modifyFeedrate :: (Double -> Double) -> Code -> Code
modifyFeedrate = modifyParam F

-- |Sum of all axis distances of this 'Code'
travel :: Code -> Double
travel Code{codeCls=(Just G), ..} = M.foldl (+) 0 codeAxes
travel _ = 0

-- |Test if 'Code' belongs to group g
inGroup c g = any (\x -> x c) g

-- |Test if 'Code' belongs to any group
known c = any (\x -> inGroup c x) groups

--updateModals current c = trace (show $ zipWith (,) current $ map (\x -> inGroup c x) groups) $ zipWith maybeUpdate current $ map (\x -> inGroup c x) groups
--  where
--    maybeUpdate Nothing True = trace ("new modal" ++ show c) $ Just c
--    maybeUpdate (Just old) True = trace ("change modal" ++ show (appendAxes c old)) $ Just (appendAxes c old)
--    maybeUpdate old False = trace ("no match" ++ show old) $ old
--
updateModals current c = zipWith maybeUpdate current $ map (\x -> inGroup c x) groups
  where
    maybeUpdate Nothing True = Just c
    maybeUpdate (Just old) True =  Just (appendAxes c old)
    maybeUpdate old False =  old

appendAxes cto cfrom = appmod (axes $ appendOnlyAxes (codeAxes cto) (codeAxes cfrom)) cto

incomplete Code{codeCls=Nothing, ..} = True
incomplete Code{codeNum=Nothing, ..} = True
incomplete _ = False

totalize :: GCode -> GCode
totalize = totalize' emptyGroups
  where
    totalize' inEffect [] = []
    totalize' inEffect (x:rest) = (updateFromEffect inEffect x):(totalize' (updateModals inEffect x) rest)


type Evaluator a = State [Maybe Code] a

totalizer :: GCode -> Evaluator GCode
totalizer [] = return $ []
totalizer (x:xs) = do
  trace ("x " ++ show x) $ return ()
  cs <- get
  let nx = updateFromEffect cs x

  modify' (flip updateModals $ nx)

  --trace ("cs " ++ show cs) $ return ()
  --trace ("nx " ++ show nx) $ return ()
  rest <- totalizer xs

  return $ (nx:rest)

totalize' c = runState (totalizer c) emptyGroups

--totalizeTrace' c = do
--  (a, b) <- runState (totalizer c) emptyGroups
--  return $ (a, b)

updateFromEffect inEffect x = do
  case (!!) inEffect 0 of -- motion group
    Nothing -> x
    (Just e) -> appmod (
         (cls $ fromJust $ codeCls e)
      <> (num $ fromJust $ codeNum e)
      <> (axes $ appendOnlyAxes (codeAxes x) (codeAxes e))
      ) x

updateFromEffect _ x | otherwise = x

updateIncompleteFromEffect inEffect x | incomplete x = do
  case (!!) inEffect 0 of -- motion group
    Nothing -> x
    (Just e) -> appmod (
         (cls $ fromJust $ codeCls e)
      <> (num $ fromJust $ codeNum e)
      <> (axes $ appendOnlyAxes (codeAxes x) (codeAxes e))
      ) x

updateIncompleteFromEffect _ x | otherwise = x

emptyGroups = map (pure Nothing) groups

-- update axes that aren't defined in target
appendOnlyAxes target from = M.union target missingOnly
  where missingOnly = M.difference from target

rot by x y = (x * (cos by) - y * (sin by), y * (cos by) + x * (sin by))

roundprec n x = (fromInteger $ round $ x * (10^n)) / (10.0^^n)

updateLimitsCode :: Limits -> Code -> Limits
updateLimitsCode s Code{..} = updateLimits s codeAxes
updateLimitsCode s _ = s

updateLimits :: Limits -> Axes -> Limits
updateLimits s = M.foldlWithKey adj s
  where
    adj limits ax val = M.alter (alterfn val) ax limits
    alterfn val (Just (min_c, max_c)) = Just (min min_c val, max max_c val)
    alterfn val Nothing = Just (val, val)