content: {-# LANGUAGE DeriveAnyClass #-}
  {-# LANGUAGE GADTs #-}
  {-# LANGUAGE InstanceSigs #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE RankNTypes #-}
  
  module DataFrame.Errors where
  
  import qualified Data.Text as T
  import qualified Data.Vector.Unboxed as VU
  
  import Control.Exception
  import Data.Array
  import Data.Typeable (Typeable)
  import DataFrame.Display.Terminal.Colours
  import Type.Reflection (TypeRep)
  
  data TypeErrorContext a b = MkTypeErrorContext
      { userType :: Either String (TypeRep a)
      , expectedType :: Either String (TypeRep b)
      , errorColumnName :: Maybe String
      , callingFunctionName :: Maybe String
      }
  
  data DataFrameException where
      TypeMismatchException ::
          forall a b.
          (Typeable a, Typeable b) =>
          TypeErrorContext a b ->
          DataFrameException
      AggregatedAndNonAggregatedException :: T.Text -> T.Text -> DataFrameException
      ColumnNotFoundException :: T.Text -> T.Text -> [T.Text] -> DataFrameException
      EmptyDataSetException :: T.Text -> DataFrameException
      InternalException :: T.Text -> DataFrameException
      NonColumnReferenceException :: T.Text -> DataFrameException
      UnaggregatedException :: T.Text -> DataFrameException
      WrongQuantileNumberException :: Int -> DataFrameException
      WrongQuantileIndexException :: VU.Vector Int -> Int -> DataFrameException
      deriving (Exception)
  
  instance Show DataFrameException where
      show :: DataFrameException -> String
      show (TypeMismatchException context) =
          let
              errorString =
                  typeMismatchError
                      (either id show (userType context))
                      (either id show (expectedType context))
           in
              addCallPointInfo
                  (errorColumnName context)
                  (callingFunctionName context)
                  errorString
      show (ColumnNotFoundException columnName callPoint availableColumns) = columnNotFound columnName callPoint availableColumns
      show (EmptyDataSetException callPoint) = emptyDataSetError callPoint
      show (WrongQuantileNumberException q) = wrongQuantileNumberError q
      show (WrongQuantileIndexException qs q) = wrongQuantileIndexError qs q
      show (InternalException msg) = "Internal error: " ++ T.unpack msg
      show (NonColumnReferenceException msg) = "Expression must be a column reference in: " ++ T.unpack msg
      show (UnaggregatedException expr) = "Expression is not fully aggregated: " ++ T.unpack expr
      show (AggregatedAndNonAggregatedException expr1 expr2) =
          "Cannot combine aggregated and non-aggregated expressions: \n"
              ++ T.unpack expr1
              ++ "\n"
              ++ T.unpack expr2
  
  columnNotFound :: T.Text -> T.Text -> [T.Text] -> String
  columnNotFound name callPoint columns =
      red "\n\n[ERROR] "
          ++ "Column not found: "
          ++ T.unpack name
          ++ " for operation "
          ++ T.unpack callPoint
          ++ "\n\tDid you mean "
          ++ T.unpack (guessColumnName name columns)
          ++ "?\n\n"
  
  typeMismatchError :: String -> String -> String
  typeMismatchError givenType expectedType =
      red $
          red "\n\n[Error]: Type Mismatch"
              ++ "\n\tWhile running your code I tried to "
              ++ "get a column of type: "
              ++ red (show givenType)
              ++ " but the column in the dataframe was actually of type: "
              ++ green (show expectedType)
  
  emptyDataSetError :: T.Text -> String
  emptyDataSetError callPoint =
      red "\n\n[ERROR] "
          ++ T.unpack callPoint
          ++ " cannot be called on empty data sets"
  
  wrongQuantileNumberError :: Int -> String
  wrongQuantileNumberError q =
      red "\n\n[ERROR] "
          ++ "Quantile number q should satisfy "
          ++ "q >= 2, but here q is "
          ++ show q
  
  wrongQuantileIndexError :: VU.Vector Int -> Int -> String
  wrongQuantileIndexError qs q =
      red "\n\n[ERROR] "
          ++ "For quantile number q, "
          ++ "each quantile index i "
          ++ "should satisfy 0 <= i <= q, "
          ++ "but here q is "
          ++ show q
          ++ " and indexes are "
          ++ show qs
  
  addCallPointInfo :: Maybe String -> Maybe String -> String -> String
  addCallPointInfo (Just name) (Just cp) err =
      err
          ++ ( "\n\tThis happened when calling function "
                  ++ brightGreen cp
                  ++ " on "
                  ++ brightGreen name
             )
  addCallPointInfo Nothing (Just cp) err =
      err
          ++ ( "\n\tThis happened when calling function "
                  ++ brightGreen cp
             )
  addCallPointInfo (Just name) Nothing err =
      err
          ++ ( "\n\tOn "
                  ++ name
                  ++ "\n\n"
             )
  addCallPointInfo Nothing Nothing err = err
  
  guessColumnName :: T.Text -> [T.Text] -> T.Text
  guessColumnName userInput columns = case map (\k -> (editDistance userInput k, k)) columns of
      [] -> ""
      res -> (snd . minimum) res
  
  editDistance :: T.Text -> T.Text -> Int
  editDistance xs ys = table ! (m, n)
    where
      (m, n) = (T.length xs, T.length ys)
      x = array (1, m) (zip [1 ..] (T.unpack xs))
      y = array (1, n) (zip [1 ..] (T.unpack ys))
  
      table :: Array (Int, Int) Int
      table = array bnds [(ij, dist ij) | ij <- range bnds]
      bnds = ((0, 0), (m, n))
  
      dist (0, j) = j
      dist (i, 0) = i
      dist (i, j) =
          minimum
              [ table ! (i - 1, j) + 1
              , table ! (i, j - 1) + 1
              , if x ! i == y ! j then table ! (i - 1, j - 1) else 1 + table ! (i - 1, j - 1)
              ]
  