{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveGeneric #-}
module Codec.Xlsx.Writer.Internal.PivotTable
  ( PivotTableFiles(..)
  , renderPivotTableFiles
  ) where

import Data.ByteString.Lazy (ByteString)
import Data.List (elemIndex, transpose)
import Data.List.Extra (nubOrd)
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.Text (Text)
import GHC.Generics (Generic)
import Safe (fromJustNote)
import Text.XML

import Codec.Xlsx.Types.Cell
import Codec.Xlsx.Types.Common
import Codec.Xlsx.Types.Internal
import Codec.Xlsx.Types.Internal.Relationships (odr)
import Codec.Xlsx.Types.PivotTable
import Codec.Xlsx.Types.PivotTable.Internal
import Codec.Xlsx.Writer.Internal

data PivotTableFiles = PivotTableFiles
  { pvtfTable :: ByteString
  , pvtfCacheDefinition :: ByteString
  , pvtfCacheRecords :: ByteString
  } deriving (Eq, Show, Generic)

data CacheDefinition = CacheDefinition
  { cdSourceRef :: CellRef
  , cdSourceSheet :: Text
  , cdFields :: [CacheField]
  } deriving (Eq, Show, Generic)

renderPivotTableFiles :: CellMap -> Int -> PivotTable -> PivotTableFiles
renderPivotTableFiles cm cacheId t = PivotTableFiles {..}
  where
    pvtfTable = renderLBS def $ ptDefinitionDocument cacheId cache t
    cache = generateCache cm t
    (cacheDoc, cacheRecordsDoc) = writeCache cache
    pvtfCacheDefinition = renderLBS def cacheDoc
    pvtfCacheRecords = renderLBS def cacheRecordsDoc

ptDefinitionDocument :: Int -> CacheDefinition -> PivotTable -> Document
ptDefinitionDocument cacheId cache t =
    documentFromElement "Pivot table generated by xlsx" $
    ptDefinitionElement "pivotTableDefinition" cacheId cache t

ptDefinitionElement :: Name -> Int -> CacheDefinition -> PivotTable -> Element
ptDefinitionElement nm cacheId cache PivotTable {..} =
  elementList nm attrs elements
  where
    attrs =
      catMaybes
        [ "colGrandTotals" .=? justFalse _pvtColumnGrandTotals
        , "rowGrandTotals" .=? justFalse _pvtRowGrandTotals
        , "outline" .=? justTrue _pvtOutline
        , "outlineData" .=? justTrue _pvtOutlineData
        ] ++
      [ "name" .= _pvtName
      , "dataCaption" .= _pvtDataCaption
      , "cacheId" .= cacheId
      , "dataOnRows" .= (DataPosition `elem` _pvtRowFields)
      ]
    elements = [location, pivotFields, rowFields, colFields, dataFields]
    location =
      leafElement
        "location"
        [ "ref" .= _pvtLocation
          -- TODO : set proper
        , "firstHeaderRow" .= (1 :: Int)
        , "firstDataRow" .= (2 :: Int)
        , "firstDataCol" .= (1 :: Int)
        ]
    name2x = M.fromList $ zip (mapMaybe _pfiName _pvtFields) [0 ..]
    mapFieldToX f = fromJustNote "no field" $ M.lookup f name2x
    pivotFields = elementListSimple "pivotFields" $ map pFieldEl _pvtFields
    maybeFieldIn Nothing _ = False
    maybeFieldIn (Just name) positions = FieldPosition name `elem` positions
    pFieldEl PivotFieldInfo { _pfiName = fName
                            , _pfiOutline = outline
                            , _pfiSortType = sortType
                            , _pfiHiddenItems = hidden
                            }
      | fName `maybeFieldIn` _pvtRowFields =
        pFieldEl' fName outline ("axisRow" :: Text) hidden sortType
      | fName `maybeFieldIn` _pvtColumnFields =
        pFieldEl' fName outline ("axisCol" :: Text) hidden sortType
      | otherwise =
        leafElement "pivotField" $
        [ "dataField" .= True
        , "showAll" .= False
        , "outline" .= outline] ++
        catMaybes ["name" .=? fName]
    pFieldEl' fName outline axis hidden sortType =
      elementList
        "pivotField"
        ([ "axis" .= axis
         , "showAll" .= False
         , "outline" .= outline
         ] ++
         catMaybes [ "name" .=? fName
                   , "sortType" .=? justNonDef FieldSortManual sortType])
        [ elementListSimple "items" $
          items fName hidden ++
          [leafElement "item" ["t" .= ("default" :: Text)]]
        ]
    items Nothing _ = []
    items (Just fName) hidden =
      [ itemEl x item hidden
      | (x, item) <- zip [0 ..] . fromMaybe [] $ M.lookup fName cachedItems
      ]
    itemEl x item hidden =
      leafElement "item" $
      ["x" .= (x :: Int)] ++ catMaybes ["h" .=? justTrue (item `elem` hidden)]
    cachedItems =
      M.fromList $ [(cfName, cfItems) | CacheField {..} <- cdFields cache]
    rowFields =
      elementListSimple "rowFields" . map fieldEl $
      if length _pvtDataFields > 1
        then _pvtRowFields
        else filter (/= DataPosition) _pvtRowFields
    colFields = elementListSimple "colFields" $ map fieldEl _pvtColumnFields
    fieldEl p = leafElement "field" ["x" .= fieldPos p]
    fieldPos DataPosition = (-2) :: Int
    fieldPos (FieldPosition f) = mapFieldToX f
    dataFields = elementListSimple "dataFields" $ map dFieldEl _pvtDataFields
    dFieldEl DataField {..} =
      leafElement "dataField" $
      catMaybes
        [ "name" .=? Just _dfName
        , "fld" .=? Just (mapFieldToX _dfField)
        , "subtotal" .=? justNonDef ConsolidateSum _dfFunction
        ]

generateCache :: CellMap -> PivotTable -> CacheDefinition
generateCache cm PivotTable {..} =
  CacheDefinition
  { cdSourceRef = _pvtSrcRef
  , cdSourceSheet = _pvtSrcSheet
  , cdFields = cachedFields
  }
  where
    cachedFields = mapMaybe (fmap cache . _pfiName) _pvtFields
    cache name =
      CacheField
      { cfName = name
      , cfItems =
          fromJustNote "specified pivot table field does not exist" $
          M.lookup name itemsByName
      }
    ((r1, c1), (r2, c2)) =
      fromJustNote "Invalid src ref of pivot table " $ fromRange _pvtSrcRef
    getCellValue ix = M.lookup ix cm >>= _cellValue
    itemsByName =
      M.fromList $
      flip mapMaybe [c1 .. c2] $ \c -> do
        CellText nm <- getCellValue (r1, c)
        let values = mapMaybe (\r -> getCellValue (r, c)) [(r1 + 1) .. r2]
        return (PivotFieldName nm, nubOrd values)

writeCache :: CacheDefinition -> (Document, Document)
writeCache CacheDefinition {..} = (cacheDefDoc, cacheRecordsDoc)
  where
    cacheDefDoc =
      documentFromElement "Pivot cache definition generated by xlsx" $
      elementList "pivotCacheDefinition" attrs elements
    attrs = ["invalid" .= True, "refreshOnLoad" .= True, odr "id" .= unsafeRefId 1]
    elements = [worksheetSource, cacheFields]
    worksheetSource =
      elementList
        "cacheSource"
        ["type" .= ("worksheet" :: Text)]
        [ leafElement
            "worksheetSource"
            ["ref" .= cdSourceRef, "sheet" .= cdSourceSheet]
        ]
    cacheFields =
      elementListSimple "cacheFields" $ map (toElement "cacheField") cdFields
    cacheRecordsDoc =
      documentFromElement "Pivot cache records generated by xlsx" .
      elementListSimple "pivotCacheRecords" $
      map (elementListSimple "r" . map recordValueToEl) cacheRecords
    recordValueToEl (CacheText t) = leafElement "s" ["v" .= t]
    recordValueToEl (CacheNumber n) = leafElement "n" ["v" .= n]
    recordValueToEl (CacheIndex i) = leafElement "x" ["v" .= i]
    cacheRecords = transpose $ map (itemsToRecordValues . cfItems) cdFields
    itemsToRecordValues vals =
      if all isText vals
        then indexes vals
        else map itemToRecordValue vals
    isText (CellText _) = True
    isText _ = False
    indexes vals =
      [ CacheIndex . fromJustNote "inconsistend definition" $ elemIndex v vals
      | v <- vals
      ]
    itemToRecordValue (CellDouble d) = CacheNumber d
    itemToRecordValue (CellText t) = CacheText t
    itemToRecordValue v = error $ "Unsupported value for pivot tables: " ++ show v
