{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
module Diff
  ( Args(..)
  , run
  )
  where


import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Name as Name
import qualified Data.NonEmptyList as NE

import qualified BackgroundWriter as BW
import qualified Build
import Deps.Diff (PackageChanges(..), ModuleChanges(..), Changes(..))
import qualified Deps.Diff as DD
import qualified Deps.Registry as Registry
import qualified Elm.Compiler.Type as Type
import qualified Elm.Details as Details
import qualified Elm.Docs as Docs
import qualified Elm.Magnitude as M
import qualified Elm.Outline as Outline
import qualified Elm.Package as Pkg
import qualified Elm.Version as V
import qualified Http
import qualified Reporting
import Reporting.Doc ((<>), (<+>))
import qualified Reporting.Doc as D
import qualified Reporting.Exit as Exit
import qualified Reporting.Exit.Help as Help
import qualified Reporting.Render.Type.Localizer as L
import qualified Reporting.Task as Task
import qualified Stuff



-- RUN


data Args
  = CodeVsLatest
  | CodeVsExactly V.Version
  | LocalInquiry V.Version V.Version
  | GlobalInquiry Pkg.Name V.Version V.Version


run :: Args -> () -> IO ()
run args () =
  Reporting.attempt Exit.diffToReport $
    Task.run $
      do  env <- getEnv
          diff env args



-- ENVIRONMENT


data Env =
  Env
    { _maybeRoot :: Maybe FilePath
    , _cache :: Stuff.PackageCache
    , _manager :: Http.Manager
    , _registry :: Registry.Registry
    }


getEnv :: Task Env
getEnv =
  do  maybeRoot <- Task.io $ Stuff.findRoot
      cache     <- Task.io $ Stuff.getPackageCache
      manager   <- Task.io $ Http.getManager
      registry  <- Task.eio Exit.DiffMustHaveLatestRegistry $ Registry.latest manager cache
      return (Env maybeRoot cache manager registry)



-- DIFF


type Task a =
  Task.Task Exit.Diff a


diff :: Env -> Args -> Task ()
diff env@(Env _ _ _ registry) args =
  case args of
    GlobalInquiry name v1 v2 ->
      case Registry.getVersions' name registry of
        Right vsns ->
          do  oldDocs <- getDocs env name vsns (min v1 v2)
              newDocs <- getDocs env name vsns (max v1 v2)
              writeDiff oldDocs newDocs

        Left suggestions ->
          Task.throw $ Exit.DiffUnknownPackage name suggestions

    LocalInquiry v1 v2 ->
      do  (name, vsns) <- readOutline env
          oldDocs <- getDocs env name vsns (min v1 v2)
          newDocs <- getDocs env name vsns (max v1 v2)
          writeDiff oldDocs newDocs

    CodeVsLatest ->
      do  (name, vsns) <- readOutline env
          oldDocs <- getLatestDocs env name vsns
          newDocs <- generateDocs env
          writeDiff oldDocs newDocs

    CodeVsExactly version ->
      do  (name, vsns) <- readOutline env
          oldDocs <- getDocs env name vsns version
          newDocs <- generateDocs env
          writeDiff oldDocs newDocs



-- GET DOCS


getDocs :: Env -> Pkg.Name -> Registry.KnownVersions -> V.Version -> Task Docs.Documentation
getDocs (Env _ cache manager _) name (Registry.KnownVersions latest previous) version =
  if latest == version || elem version previous
  then Task.eio (Exit.DiffDocsProblem version) $ DD.getDocs cache manager name version
  else Task.throw $ Exit.DiffUnknownVersion name version (latest:previous)


getLatestDocs :: Env -> Pkg.Name -> Registry.KnownVersions -> Task Docs.Documentation
getLatestDocs (Env _ cache manager _) name (Registry.KnownVersions latest _) =
  Task.eio (Exit.DiffDocsProblem latest) $ DD.getDocs cache manager name latest



-- READ OUTLINE


readOutline :: Env -> Task (Pkg.Name, Registry.KnownVersions)
readOutline (Env maybeRoot _ _ registry) =
  case maybeRoot of
    Nothing ->
      Task.throw $ Exit.DiffNoOutline

    Just root ->
      do  result <- Task.io $ Outline.read root
          case result of
            Left err ->
              Task.throw $ Exit.DiffBadOutline err

            Right outline ->
              case outline of
                Outline.App _ ->
                  Task.throw $ Exit.DiffApplication

                Outline.Pkg (Outline.PkgOutline pkg _ _ _ _ _ _ _) ->
                  case Registry.getVersions pkg registry of
                    Just vsns -> return (pkg, vsns)
                    Nothing   -> Task.throw Exit.DiffUnpublished



-- GENERATE DOCS


generateDocs :: Env -> Task Docs.Documentation
generateDocs (Env maybeRoot _ _ _) =
  case maybeRoot of
    Nothing ->
      Task.throw $ Exit.DiffNoOutline

    Just root ->
      do  details <-
            Task.eio Exit.DiffBadDetails $ BW.withScope $ \scope ->
              Details.load Reporting.silent scope root

          case Details._outline details of
            Details.ValidApp _ ->
              Task.throw $ Exit.DiffApplication

            Details.ValidPkg _ exposed _ ->
              case exposed of
                [] ->
                  Task.throw $ Exit.DiffNoExposed

                e:es ->
                  Task.eio Exit.DiffBadBuild $
                    Build.fromExposed Reporting.silent root details Build.KeepDocs (NE.List e es)



-- WRITE DIFF


writeDiff :: Docs.Documentation -> Docs.Documentation -> Task ()
writeDiff oldDocs newDocs =
  let
    changes = DD.diff oldDocs newDocs
    localizer = L.fromNames (Map.union oldDocs newDocs)
  in
  Task.io $ Help.toStdout $ toDoc localizer changes <> "\n"



-- TO DOC


toDoc :: L.Localizer -> PackageChanges -> D.Doc
toDoc localizer changes@(PackageChanges added changed removed) =
  if null added && Map.null changed && null removed then
    "No API changes detected, so this is a" <+> D.green "PATCH" <+> "change."
  else
    let
      magDoc =
        D.fromChars (M.toChars (DD.toMagnitude changes))

      header =
        "This is a" <+> D.green magDoc <+> "change."

      addedChunk =
        if null added then [] else
          [ Chunk "ADDED MODULES" M.MINOR $
              D.vcat $ map D.fromName added
          ]

      removedChunk =
        if null removed then [] else
          [ Chunk "REMOVED MODULES" M.MAJOR $
              D.vcat $ map D.fromName removed
          ]

      chunks =
        addedChunk ++ removedChunk ++ map (changesToChunk localizer) (Map.toList changed)
    in
      D.vcat (header : "" : map chunkToDoc chunks)


data Chunk =
  Chunk
    { _title :: String
    , _magnitude :: M.Magnitude
    , _details :: D.Doc
    }


chunkToDoc :: Chunk -> D.Doc
chunkToDoc (Chunk title magnitude details) =
  let
    header =
      "----" <+> D.fromChars title <+> "-" <+> D.fromChars (M.toChars magnitude) <+> "----"
  in
    D.vcat
      [ D.dullcyan header
      , ""
      , D.indent 4 details
      , ""
      , ""
      ]


changesToChunk :: L.Localizer -> (Name.Name, ModuleChanges) -> Chunk
changesToChunk localizer (name, changes@(ModuleChanges unions aliases values binops)) =
  let
    magnitude =
      DD.moduleChangeMagnitude changes

    (unionAdd, unionChange, unionRemove) =
      changesToDocTriple (unionToDoc localizer) unions

    (aliasAdd, aliasChange, aliasRemove) =
      changesToDocTriple (aliasToDoc localizer) aliases

    (valueAdd, valueChange, valueRemove) =
      changesToDocTriple (valueToDoc localizer) values

    (binopAdd, binopChange, binopRemove) =
      changesToDocTriple (binopToDoc localizer) binops
  in
    Chunk (Name.toChars name) magnitude $
      D.vcat $ List.intersperse "" $ Maybe.catMaybes $
        [ changesToDoc "Added" unionAdd aliasAdd valueAdd binopAdd
        , changesToDoc "Removed" unionRemove aliasRemove valueRemove binopRemove
        , changesToDoc "Changed" unionChange aliasChange valueChange binopChange
        ]


changesToDocTriple :: (k -> v -> D.Doc) -> Changes k v -> ([D.Doc], [D.Doc], [D.Doc])
changesToDocTriple entryToDoc (Changes added changed removed) =
  let
    indented (name, value) =
      D.indent 4 (entryToDoc name value)

    diffed (name, (oldValue, newValue)) =
      D.vcat
        [ "  - " <> entryToDoc name oldValue
        , "  + " <> entryToDoc name newValue
        , ""
        ]
  in
    ( map indented (Map.toList added)
    , map diffed   (Map.toList changed)
    , map indented (Map.toList removed)
    )


changesToDoc :: String -> [D.Doc] -> [D.Doc] -> [D.Doc] -> [D.Doc] -> Maybe D.Doc
changesToDoc categoryName unions aliases values binops =
  if null unions && null aliases && null values && null binops then
    Nothing

  else
    Just $ D.vcat $
      D.fromChars categoryName <> ":" : unions ++ aliases ++ binops ++ values


unionToDoc :: L.Localizer -> Name.Name -> Docs.Union -> D.Doc
unionToDoc localizer name (Docs.Union _ tvars ctors) =
  let
    setup =
      "type" <+> D.fromName name <+> D.hsep (map D.fromName tvars)

    ctorDoc (ctor, tipes) =
      typeDoc localizer (Type.Type ctor tipes)
  in
    D.hang 4 (D.sep (setup : zipWith (<+>) ("=" : repeat "|") (map ctorDoc ctors)))


aliasToDoc :: L.Localizer -> Name.Name -> Docs.Alias -> D.Doc
aliasToDoc localizer name (Docs.Alias _ tvars tipe) =
  let
    declaration =
      "type" <+> "alias" <+> D.hsep (map D.fromName (name:tvars)) <+> "="
  in
    D.hang 4 (D.sep [ declaration, typeDoc localizer tipe ])


valueToDoc :: L.Localizer -> Name.Name -> Docs.Value -> D.Doc
valueToDoc localizer name (Docs.Value _ tipe) =
  D.hang 4 $ D.sep [ D.fromName name <+> ":", typeDoc localizer tipe ]


binopToDoc :: L.Localizer -> Name.Name -> Docs.Binop -> D.Doc
binopToDoc localizer name (Docs.Binop _ tipe associativity (Docs.Precedence n)) =
    "(" <> D.fromName name <> ")" <+> ":" <+> typeDoc localizer tipe <> D.black details
  where
    details =
      "    (" <> D.fromName assoc <> "/" <> D.fromInt n <> ")"

    assoc =
      case associativity of
        Docs.Left  -> "left"
        Docs.Non   -> "non"
        Docs.Right -> "right"


typeDoc :: L.Localizer -> Type.Type -> D.Doc
typeDoc localizer tipe =
  Type.toDoc localizer Type.None tipe
