content: {-|
  Copyright  :  (C) 2022,      Google Inc.,
  License    :  BSD2 (see the file LICENSE)
  Maintainer :  QBayLogic B.V. <devops@qbaylogic.com>
  
  Utilities and definitions to deal with Verilog's time unit. These definitions
  are here mostly to deal with varying @`timescale@ defintions, see:
  
    https://www.chipverify.com/verilog/verilog-timescale
  
  -}
  
  {-# LANGUAGE DeriveAnyClass #-}
  {-# LANGUAGE DeriveGeneric #-}
  {-# LANGUAGE NamedFieldPuns #-}
  {-# LANGUAGE TypeFamilies #-}
  
  -- TryDomain instance
  {-# OPTIONS_GHC -Wno-deprecations #-}
  
  module Clash.Backend.Verilog.Time where
  
  import Clash.Class.HasDomain.HasSingleDomain
    (TryDomain, TryDomainResult(NotFound))
  
  import Control.DeepSeq (NFData)
  import Data.Char (toLower, isDigit)
  import Data.Hashable (Hashable)
  import Data.List (find)
  import Data.Word (Word64)
  import GHC.Generics (Generic)
  import Text.Read (readMaybe)
  
  -- | Verilog time units
  data Unit = Fs | Ps | Ns | Us | Ms | S
    deriving (Show, Enum, Bounded, Eq, Ord, Generic, Hashable, NFData)
  
  type instance TryDomain t Unit = 'NotFound
  
  -- | Verilog time period. A combination of a length and a unit.
  data Period = Period Word64 Unit
    deriving (Show, Generic, Hashable, Eq, NFData)
  
  -- | Verilog timescale. Influences simulation precision.
  data Scale = Scale
    { -- | Time step in wait statements, e.g. `#1`.
      step :: Period
  
      -- | Simulator precision - all units will get rounded to this period.
    , precision :: Period
    }
    deriving (Show, Generic, Hashable, Eq, NFData)
  
  -- | Pretty print 'Scale' to Verilog `timescale
  --
  -- >>> scaleToString (Scale (Period 100 Ps) (Period 10 Fs))
  -- "`timescale 100ps/10fs"
  --
  scaleToString :: Scale -> String
  scaleToString (Scale{step, precision}) =
    "`timescale " <> periodToString step <> "/" <> periodToString precision
  
  -- | Convert 'Unit' to Verilog time unit
  --
  -- >>> periodToString (Period 100 Fs)
  -- "100fs"
  --
  periodToString :: Period -> String
  periodToString (Period len unit) = show len <> unitToString unit
  
  -- | Convert 'Unit' to Verilog time unit
  --
  -- >>> unitToString Ms
  -- "ms"
  --
  unitToString :: Unit -> String
  unitToString = map toLower . show
  
  -- | Parse string representing a Verilog time unit to 'Unit'.
  --
  -- >>> parseUnit "ms"
  -- Just Ms
  -- >>> parseUnit "xs"
  -- Nothing
  --
  parseUnit :: String -> Maybe Unit
  parseUnit s = find tryUnit [minBound..]
   where
    tryUnit :: Unit -> Bool
    tryUnit u = unitToString u == s
  
  -- | Parse a Verilog
  --
  -- >>> parsePeriod "100ms"
  -- Just (Period 100 Ms)
  -- >>> parsePeriod "100xs"
  -- Nothing
  -- >>> parsePeriod "100"
  -- Nothing
  -- >>> parsePeriod "ms"
  -- Nothing
  --
  parsePeriod :: String -> Maybe Period
  parsePeriod s =
    case span isDigit s of
      (len0, unit0) -> do
        len1 <- readMaybe len0
        unit1 <- parseUnit unit0
        pure (Period len1 unit1)
  
  -- | Convert a period to a specific time unit. Will always output a minimum
  -- of 1, even if the given 'Period' is already of the right 'Unit'.
  --
  -- >>> convertUnit Ps (Period 100 Ps)
  -- 100
  -- >>> convertUnit Fs (Period 100 Ps)
  -- 100000
  -- >>> convertUnit Ns (Period 100 Ps)
  -- 1
  -- >>> convertUnit Ms (Period 0 Ms)
  -- 1
  --
  convertUnit :: Unit -> Period -> Word64
  convertUnit targetUnit = go
   where
    go (Period len unit) =
      case compare unit targetUnit of
        LT -> go (Period (len `div` 1000) (succ unit))
        EQ -> max 1 len
        GT -> go (Period (len * 1000) (pred unit))
  