module Data.GCode.Canon.Convert where

import Control.Applicative

import Data.GCode.Types (Code(..), Class(..), Axes, ParamDesignator(..))
import Data.GCode.Canon (Canon(..))

import qualified Data.Map
import qualified Data.GCode.Canon as C
import qualified Data.GCode.Types as T

import Data.GCode.RS274
import Data.GCode.Utils

-- | Convert code to its canonical representation
toCanon :: Code -> [Canon]
toCanon c | isRapid c =
    ifHasParam F c C.SetTraverseRate
 <> ifNonEmptyAxes c C.StraightTraverse
toCanon c | isMove c =
    ifHasParam F c C.SetFeedRate
 <> ifNonEmptyAxes c C.StraightFeed

-- :((
--toCanon c | isArc c = ArcFeed

toCanon c | isCoordinateSystemOffset c = pure $ C.SetCoords (codeAxes c)

toCanon c | isDwell c
  = pure . C.PauseSeconds $ getParamOrFail P c "No P for Dwell"

-- Converted by step
toCanon c | isMillimeters c = empty
toCanon c | isInches c      = empty
toCanon c | isAbsolute c    = empty
toCanon c | isRelative c    = empty

-- Planes
toCanon c | isXYPlane c = pure $ C.PlaneSelect C.XY
toCanon c | isZXPlane c = pure $ C.PlaneSelect C.ZX
toCanon c | isYZPlane c = pure $ C.PlaneSelect C.YZ
toCanon c | isUVPlane c = pure $ C.PlaneSelect C.UV
toCanon c | isWUPlane c = pure $ C.PlaneSelect C.WU
toCanon c | isVWPlane c = pure $ C.PlaneSelect C.VW

-- Standalone
toCanon Code { codeCls = Just FStandalone, codeNum = Just newFeed }
  = pure $ C.SetFeedRate $ fromIntegral newFeed
toCanon Code { codeCls = Just SStandalone, codeNum = Just spindleRPM }
  = pure $ C.SpindleSpeed $ fromIntegral spindleRPM

-- Units
toCanon c | isUnitsPerMinute c     = empty
toCanon c | isUnitsPerRevolution c = error "Don't know how to handle units per revolution"

-- Spindle
toCanon c | isSpindleCW c = pure C.SpindleStart
  { spindleDirection = C.ClockWise
  , spindleWaitForSpeed = True } -- questionable
toCanon c | isSpindleCCW c = pure C.SpindleStart
  { spindleDirection = C.CounterClockWise
  , spindleWaitForSpeed = True } -- questionable
toCanon c | isSpindleStop c = pure C.SpindleStop

-- Coolant
toCanon c | isCoolantMist  c = pure C.CoolantMist
toCanon c | isCoolantFlood c = pure C.CoolantFlood
toCanon c | isCoolantStop  c = pure C.CoolantStop

-- Tool
toCanon c | isToolChange c = pure C.ToolChange
toCanon (Code{codeCls=(Just T), codeNum=(Just toolId)}) = pure $ C.ToolSelect toolId
toCanon c | isToolLength c        = pure $ C.ToolLengthCompensation C.LengthTable
toCanon c | isToolLengthDynamic c = pure $ C.ToolLengthCompensation $ C.Dynamic
  (codeAxes c)
toCanon c | isToolLengthAdd c     = pure $ C.ToolLengthCompensation $ C.Add
  (round $ getParamOrFail H c "Add tool change offset requires H parameter of the tool to grab offset from")
toCanon c | isToolLengthCancel c  = pure $ C.ToolLengthCompensation C.NoCompensation

-- Printer -- XXX: needs handling in step
toCanon c | isExtruderAbsolute c = empty
toCanon c | isExtruderRelative c = empty

-- Printer heating
toCanon c | isSetExtruderTemperature c = pure $ C.SetTemperature
  (C.Heater C.HeatedExtruder $ round <$> getParam P c)
  (getParamOrFail S c "Set extruder temperature command missing S parameter for temperature value")
toCanon c | isSetBedTemperature c = pure $ C.SetTemperature
  (C.Heater C.HeatedBed $ round <$> getParam P c)
  (getParamOrFail S c "Set bed temperature command missing S parameter for temperature value")
toCanon c | isSetChamberTemperature c = pure $ C.SetTemperature
  (C.Heater C.HeatedChamber $ round <$> getParam P c)
  (getParamOrFail S c "Set heated chamber temperature command missing S parameter for temperature value")
toCanon c | isCancelWaitTemperature c = pure $ C.CancelWaitTemperature
-- Wait variants
toCanon c | isSetExtruderTemperatureAndWait c = pure $ C.SetTemperatureWait
  (C.Heater C.HeatedExtruder $ round <$> getParam P c)
  (getParamOrFail S c "Set extruder temperature and wait command missing S parameter for temperature value")
toCanon c | isSetBedTemperatureAndWait c = pure $ C.SetTemperatureWait
  (C.Heater C.HeatedBed $ round <$> getParam P c)
  (getParamOrFail S c "Set bed temperature and wait command missing S parameter for temperature value")
toCanon c | isSetChamberTemperatureAndWait c = pure $ C.SetTemperatureWait
  (C.Heater C.HeatedChamber $ round <$> getParam P c)
  (getParamOrFail S c "Set chamber temperature and wait command missing S parameter for temperature value")
-- Cancel
toCanon c | isCancelWaitTemperature c = pure $ C.CancelWaitTemperature

-- Printer cooling
toCanon c | isFanOn c = pure C.FanOn
toCanon c | isFanOff c = pure C.FanOff

-- Printer homing, XXX: this clashes with G28 of cnc which is StoredPositionMove
toCanon c | isGN 28 c = empty

-- Printer leveling
toCanon c | isAutoBedLevel c = pure C.LevelBed

-- Printer miscs, XXX: we probably can't even parse M117 Hello world
toCanon c | isDisplayMessage c = empty
toCanon c | isDisableActuators c = pure $ C.DisableMotors (codeAxes c)

toCanon c | isProgramEnd c = pure C.ProgramEnd
toCanon c | isCommentOnly c = pure $ C.Comment (codeComment c) -- XXX: strip spaces
toCanon (T.Comment c) = pure $ C.Comment c
toCanon Empty     = empty
toCanon (Other _) = empty -- questionable
-- this is bad but we can't use GHC to tell us about missing clauses
-- due to how Code type is freeform-ish (which is also the reason for Canon).
-- Lets stay on the safe side and error for now as ignoring could lead to
-- missing important commands.
toCanon c = error $ "No canon for " ++ show c

-- Helpers

-- Apply @f@ to parameter value only iff @p@ parameter is found, mempty otherwise
ifHasParam :: (Monoid (f a), Applicative f)
           => ParamDesignator
           -> Code
           -> (Double -> a)
           -> f a
ifHasParam p c f = case getParam p c of
  Nothing -> mempty
  Just val -> pure $ f val

-- Apply @f@ to `Axes` value only iff `Code` has axes, mempty otherwise
ifNonEmptyAxes :: (Applicative f, Monoid (f a))
               => Code
               -> (Axes -> a)
               -> f a
ifNonEmptyAxes c f | codeAxes c /= mempty = pure $ f (codeAxes c)
ifNonEmptyAxes _ _ | otherwise = mempty

-- Get parameter value or fail with `error`, useful for required parameters
getParamOrFail :: ParamDesignator
               -> Code
               -> [Char]
               -> Double
getParamOrFail param code msg = maybe (error msg) id (getParam param code)

-- brr
isCommentOnly :: Code -> Bool
isCommentOnly (Code { codeCls = Nothing
                    , codeNum = Nothing
                    , codeSub = Nothing
                    , codeAxes = a
                    , codeParams = p
                    , codeComment = x }) |
                      Data.Map.null a && Data.Map.null p &&
                      x /= mempty = True
isCommentOnly _ = False