module Reporting.Render.Type
  ( Localizer
  , decl
  , annotation
  , toDoc
  , diffToDocs
  , Style(..)
  )
  where

import Control.Arrow ((***), first)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Text.PrettyPrint.ANSI.Leijen
  ( Doc, (<+>), cat, colon, comma, dullyellow, equals, hang, hsep
  , lbrace, lparen, parens, rbrace, rparen, sep, text, vcat
  )

import qualified AST.Helpers as Help
import qualified AST.Type as Type
import qualified AST.Variable as Var
import qualified Reporting.Error.Helpers as Help



-- PUBLIC API FOR CREATING DOCS


toDoc :: Localizer -> Type.Canonical -> Doc
toDoc localizer tipe =
  docType localizer None tipe


diffToDocs :: Localizer -> Type.Canonical -> Type.Canonical -> (Doc,Doc)
diffToDocs localizer leftType rightType =
  case diff localizer None leftType rightType of
    Same doc ->
        (doc, doc)

    Diff leftDoc rightDoc ->
        (leftDoc, rightDoc)


decl :: Localizer -> String -> [String] -> [(String, [Type.Canonical])] -> Doc
decl localizer name vars tags =
  let
    docTag (tag, args) =
      hang 2 (sep (text tag : map (docType localizer App) args))
  in
    hang 4 $ vcat $
      (hsep (map text ("type" : name : vars)))
      :
      zipWith (<+>)
        (equals : repeat (text "|"))
        (map docTag tags)


annotation :: Localizer -> String -> Type.Canonical -> Doc
annotation localizer name tipe =
  let
    docName =
      if Help.isOp name then parens (text name) else text name
  in
    case Type.collectLambdas tipe of
      parts@(_ : _ : _) ->
          hang 4 $ sep $
            docName
            : zipWith (<+>)
                (colon : repeat (text "->"))
                (map (docType localizer Func) parts)

      _ ->
          hang 4 $ sep $
            [ docName <+> colon
            , docType localizer None tipe
            ]



-- LOCALIZE TYPES


type Localizer =
  Map.Map String String


varToDoc :: Localizer -> Var.Canonical -> Doc
varToDoc localizer var =
  let
    name =
      Var.toString var
  in
    if name == "_Tuple0" then
      text "()"

    else
      text (maybe name id (Map.lookup name localizer))



-- DIFF BUILDER


data Diff a
    = Diff a a
    | Same a


instance Functor Diff where
  fmap func d =
    case d of
      Diff left right ->
          Diff (func left) (func right)

      Same both ->
          Same (func both)


instance Applicative Diff where
  pure a =
    Same a

  (<*>) func arg =
    case (func, arg) of
      (Diff leftFunc rightFunc, Diff leftArg rightArg) ->
          Diff (leftFunc leftArg) (rightFunc rightArg)

      (Diff leftFunc rightFunc, Same arg) ->
          Diff (leftFunc arg) (rightFunc arg)

      (Same func, Diff leftArg rightArg) ->
          Diff (func leftArg) (func rightArg)

      (Same func, Same arg) ->
          Same (func arg)


partitionDiffs :: Map.Map k (Diff a) -> ( [(k, (a,a))], [(k, a)] )
partitionDiffs dict =
  let
    collect key value (diffKeys, sameKeys) =
      case value of
        Diff left right ->
            ( (key, (left, right)) : diffKeys
            , sameKeys
            )

        Same both ->
            ( diffKeys
            , (key, both) : sameKeys
            )
  in
    Map.foldrWithKey collect ([],[]) dict



-- DOC DIFF


data Context
    = None
    | Func
    | App


diff :: Localizer -> Context -> Type.Canonical -> Type.Canonical -> Diff Doc
diff localizer context leftType rightType =
  let
    go = diff localizer
  in
  case (leftType, rightType) of
    (Type.Lambda _ _, Type.Lambda _ _) ->
        let
          leftParts = Type.collectLambdas leftType
          rightParts = Type.collectLambdas rightType
        in

          if length leftParts /= length rightParts then
              difference
                (docLambda context (map (docType localizer Func) leftParts))
                (docLambda context (map (docType localizer Func) rightParts))

          else
              docLambda context <$> sequenceA (zipWith (go Func) leftParts rightParts)

    (Type.Var x, Type.Var y) | x == y ->
        pure (text x)

    (Type.Type leftName, Type.Type rightName) | leftName == rightName ->
        pure (varToDoc localizer leftName)

    (Type.App (Type.Type leftName) leftArgs, Type.App (Type.Type rightName) rightArgs) ->
        if leftName /= rightName || length leftArgs /= length rightArgs then
            difference
              (docApp localizer context leftName leftArgs)
              (docApp localizer context rightName rightArgs)

        else
            let
              subContext =
                if Var.isTuple leftName then None else App
            in
              docAppHelp localizer context leftName
                <$> sequenceA (zipWith (go subContext) leftArgs rightArgs)

    (Type.App _ _, Type.App _ _) ->
        error "Type applications without concrete names should not get here."

    (Type.Record outerLeftFields outerLeftExt, Type.Record outerRightFields outerRightExt) ->
        let
          (leftFields, leftExt) =
            flattenRecord outerLeftFields outerLeftExt

          (rightFields, rightExt) =
            flattenRecord outerRightFields outerRightExt
        in
          diffRecord localizer leftFields leftExt rightFields rightExt

    (Type.Aliased leftName leftArgs _, Type.Aliased rightName rightArgs _) | leftName == rightName ->
        docAppHelp localizer context leftName
          <$> sequenceA (zipWith (go App) (map snd leftArgs) (map snd rightArgs))

    (Type.Aliased _ args real, _) ->
        go context (Type.dealias args real) rightType

    (_, Type.Aliased _ args real) ->
        go context leftType (Type.dealias args real)

    (_, _) ->
        difference
          (docType localizer context leftType)
          (docType localizer context rightType)


difference :: Doc -> Doc -> Diff Doc
difference leftDoc rightDoc =
  Diff (dullyellow leftDoc) (dullyellow rightDoc)



-- RECORD DIFFS


diffRecord :: Localizer -> Fields -> Maybe String -> Fields -> Maybe String -> Diff Doc
diffRecord localizer leftFields leftExt rightFields rightExt =
  let
    (leftOnly, both, rightOnly) =
      vennDiagram leftFields rightFields
  in
    if Map.null leftOnly && Map.null rightOnly then
        let
          fieldDiffs =
            Map.map (uncurry (diff localizer None)) both
        in
          case partitionDiffs fieldDiffs of
            ([], sames) ->
                let
                  mkRecord =
                    docRecord Full (map (first text) sames)
                in
                  if leftExt == rightExt then
                      Same (mkRecord (fmap text leftExt))

                  else
                      Diff
                        (mkRecord (fmap (dullyellow . text) leftExt))
                        (mkRecord (fmap (dullyellow . text) rightExt))

            (diffs, sames) ->
                let
                  (lefts, rights) = unzipDiffs diffs
                  style = if null sames then Full else Elide
                in
                  Diff
                    (docRecord style lefts (fmap text leftExt))
                    (docRecord style rights (fmap text rightExt))

    else
        let
          (lefts, rights) =
            analyzeFields (Map.keys leftOnly) (Map.keys rightOnly)

          style =
            if Map.null both then Full else Elide
        in
          Diff
            (docRecord style lefts (fmap text leftExt))
            (docRecord style rights (fmap text rightExt))


unzipDiffs :: [(String, (Doc, Doc))] -> ( [(Doc, Doc)], [(Doc, Doc)] )
unzipDiffs diffPairs =
  let
    unzipHelp (name, (left, right)) =
      (,) (text name, left) (text name, right)
  in
    unzip (map unzipHelp diffPairs)



-- RECORD DIFFS HELPERS


analyzeFields :: [String] -> [String] -> ( [(Doc, Doc)], [(Doc, Doc)] )
analyzeFields leftOnly rightOnly =
  let
    potentialTypos =
      Help.findPotentialTypos leftOnly rightOnly

    mkField func name =
      ( func (text name), text "..." )

    mkFieldWith counts name =
      mkField (if Set.member name counts then dullyellow else id) name
  in
    case Help.vetTypos potentialTypos of
      Just (leftTypos, rightTypos) ->
        ( map (mkFieldWith leftTypos) leftOnly
        , map (mkFieldWith rightTypos) rightOnly
        )

      Nothing ->
        ( map (mkField dullyellow) leftOnly
        , map (mkField dullyellow) rightOnly
        )


type Fields =
  Map.Map String Type.Canonical


type DoubleFields =
  Map.Map String (Type.Canonical,Type.Canonical)


vennDiagram :: Fields -> Fields -> (Fields, DoubleFields, Fields)
vennDiagram leftFields rightFields =
  ( Map.difference leftFields rightFields
  , Map.intersectionWith (,) leftFields rightFields
  , Map.difference rightFields leftFields
  )


flattenRecord
    :: [(String, Type.Canonical)]
    -> Maybe Type.Canonical
    -> (Fields, Maybe String)
flattenRecord fields ext =
  first Map.fromList (flattenRecordHelp fields ext)


flattenRecordHelp
    :: [(String, Type.Canonical)]
    -> Maybe Type.Canonical
    -> ([(String, Type.Canonical)], Maybe String)
flattenRecordHelp fields ext =
  case ext of
    Nothing ->
        (fields, Nothing)

    Just (Type.Var x) ->
        (fields, Just x)

    Just (Type.Record subFields subExt) ->
        flattenRecordHelp (fields ++ subFields) subExt

    Just (Type.Aliased _ args tipe) ->
        flattenRecordHelp fields (Just (Type.dealias args tipe))

    _ ->
        error "Trying to flatten ill-formed record."



-- TYPES TO DOCS


docType :: Localizer -> Context -> Type.Canonical -> Doc
docType localizer context tipe =
  case tipe of
    Type.Lambda _ _ ->
        docLambda context (map (docType localizer Func) (Type.collectLambdas tipe))

    Type.Var x ->
        text x

    Type.Type name ->
        varToDoc localizer name

    Type.App (Type.Type name) args ->
        docApp localizer context name args

    Type.App _ _ ->
        error "type applications should start with a type atom"

    Type.Record outerFields outerExt ->
        let
          (fields, ext) =
            flattenRecordHelp outerFields outerExt
        in
          docRecord Full
            (map (text *** docType localizer None) fields)
            (fmap text ext)

    Type.Aliased name args _ ->
        docApp localizer context name (map snd args)



docLambda :: Context -> [Doc] -> Doc
docLambda context docs =
  case docs of
    [] ->
        error "cannot call docLambda with an empty list"

    arg:rest ->
        case context of
          None ->
              sep (arg : map (text "->" <+>) rest)

          _ ->
              cat
                [ lparen
                , sep (arg : map (text "->" <+>) rest)
                , rparen
                ]



docApp :: Localizer -> Context -> Var.Canonical -> [Type.Canonical] -> Doc
docApp localizer context name args =
  let
    argContext =
      if Var.isTuple name then None else App
  in
    docAppHelp localizer context name (map (docType localizer argContext) args)


docAppHelp :: Localizer -> Context -> Var.Canonical -> [Doc] -> Doc
docAppHelp localizer context name args =
  if Var.isTuple name then
      sep
        [ cat (zipWith (<+>) (lparen : repeat comma) args)
        , rparen
        ]

  else if null args then
      varToDoc localizer name

  else
      case context of
        App ->
            cat
              [ lparen
              , hang 4 (sep (varToDoc localizer name : args))
              , rparen
              ]

        _ ->
            hang 4 (sep (varToDoc localizer name : args))



data Style
    = Elide
    | Full


docRecord :: Style -> [(Doc,Doc)] -> Maybe Doc -> Doc
docRecord style fields maybeExt =
  let
    docField (name, tipe) =
      hang 4 (sep [ name <+> colon, tipe ])

    elision =
      case style of
        Full ->
            []

        Elide ->
            [ text "..." ]

    fieldDocs =
      elision ++ map docField fields
  in
  case (fieldDocs, maybeExt) of
    ([], Nothing) ->
        text "{}"

    (_, Nothing) ->
        sep
          [ cat (zipWith (<+>) (lbrace : repeat comma) fieldDocs)
          , rbrace
          ]

    (_, Just ext) ->
        sep
          [ hang 4 $ sep $
              [ lbrace <+> ext
              , cat (zipWith (<+>) (text "|" : repeat comma) fieldDocs)
              ]
          , rbrace
          ]

