module CommonIO where

import Math
import Common(ifJust, internalError, tabDelimited, safeIndex, systemGrey)
import SafetyNet

import Graphics.UI.WX
import Graphics.UI.WXCore
import Data.List(elemIndex)
import System.Directory
import System.IO

ignoreResult :: IO a -> IO ()
ignoreResult action = do { action; return () }

-- | Writes file to disk. If writing fails, an error
--   dialog is shown and False is returned
safeWriteFile :: Window a -> String -> String -> IO Bool
safeWriteFile parentWindow fileName contents =
  do{ let tmpName = fileName ++ ".tmp"

    ; -- try to write to .tmp file
    ; writeOkay <-
        catch
            (do { writeFile tmpName contents
                ; return True
                })
            (\ioExc ->
              do{ errorDialog parentWindow "Save failed"
                            (  "Saving " ++ fileName ++ " failed.\n\n"
                            ++ "Technical reason: " ++ show ioExc ++ "\n\n"
                            ++ "Tip: do you have write permissions and enough disk space?"
                            )
                ; return False
                }
            )
    ; if not writeOkay then
        return False
      else
  do{ -- remove old file if it exists and then rename .tmp to the real name
    ; catch (do { exists <- doesFileExist fileName
                ; when exists $ removeFile fileName
                ; renameFile tmpName fileName
                ; return True
                })
        (\ioExc ->
          do{ errorDialog parentWindow "Save failed"
                (  "The file has been saved to " ++ show tmpName ++ "\nbut "
                ++ "renaming it to " ++ show fileName ++ " failed.\n\n"
                ++ "Technical reason: " ++ show ioExc
                )
            ; return False
            }
        )
    }}

strictReadFile :: String -> IO String
strictReadFile fname =
  do{ contents <- readFile fname
    ; seq (length contents) $ return contents -- force reading of entire file
    }

data TextCtrlSize = SingleLine | MultiLine

myTextDialog :: Window a -> TextCtrlSize -> String -> String -> Bool
                -> IO (Maybe String)
myTextDialog parentWindow size dialogTitle initial selectAll =
  do{ d <- dialog parentWindow [text := dialogTitle]
    ; textInput <- (case size of 
			SingleLine -> textEntry
                        MultiLine  -> textCtrl)
                         d [ alignment := AlignLeft, text := initial ]
    ; ok    <- button d [text := "Ok"]
    ; can   <- button d [text := "Cancel", identity := wxID_CANCEL]
    ; buttonSetDefault ok
    ; set d [layout :=  column 10 [ hfill $ widget textInput
                                  , floatBottomRight $ row 5 [widget ok, widget can]
                                  ]
            ]
    ; when (not selectAll) $ do set d [ visible := True ]
                                textCtrlSetInsertionPointEnd textInput
    ; showModal d $ \stop ->
                do set ok  [on command := safetyNet parentWindow $
                                          do theText <- get textInput text
                                             stop (Just theText)]
                   set can [on command := safetyNet parentWindow $ stop Nothing]
    }

-- Dialog for selecting a multiple Strings (0 or more)
-- Returns Nothing if Cancel was pressed, otherwise it returns the selected strings
multiSelectionDialog :: Window a -> String -> [String] -> [String]
                     -> IO (Maybe [String])
multiSelectionDialog parentWindow dialogTitle strings initialSelection =
  do{ d <- dialog parentWindow
            [ text := dialogTitle
            , resizeable := True
            ]
    ; p <- panel d []
    ; theListBox <- multiListBox p
        [ items := strings
        , selections :=
            [ case maybeIndex of
                Nothing -> internalError "CommonIO" "multiSelectionDialog"
                            (  "initial selection " ++ show s
                            ++ " can not be found in " ++ show strings )
                Just i  -> i
            | s <- initialSelection
            , let maybeIndex = elemIndex s strings
            ]
        ]
    ; selectAll <- button p
        [ text := "Select all"
        , on command := safetyNet parentWindow $ set theListBox [ selections := take (length strings) [0..] ]
        ]
    ; selectNone <- button p
        [ text := "Select none"
        , on command := safetyNet parentWindow $ set theListBox [ selections := [] ]
        ]
    ; ok    <- button p [text := "Ok"]
    ; can   <- button p [text := "Cancel", identity := wxID_CANCEL]
    ; buttonSetDefault ok
    ; set d [ layout := container p $
                        column 10 [ vfill $ widget theListBox
                                  , row 5 [widget selectAll, widget selectNone, widget ok, widget can]
                                  ]
            , clientSize := sz 300 400
            ]
    ; showModal d $ \stop ->
                do set ok  [on command := safetyNet parentWindow $
                              do indices <- get theListBox selections
                                 stop (Just (map (safeIndex "CommonIO.multiSelectionDialog" strings) indices))]
                   set can [on command := safetyNet parentWindow $
                                          stop Nothing]
    }

-- Dialog for selecting a single String
-- Returns Nothing if Cancel was pressed, otherwise it returns the selected string
singleSelectionDialog :: Window a -> String -> [String] -> (Maybe String)
                      -> IO (Maybe String)
singleSelectionDialog _ _ [] _ =
    internalError "CommonIO" "singleSelectionDialog" "no strings"
singleSelectionDialog parentWindow dialogTitle strings initialSelection =
  do{ d <- dialog parentWindow [ text := dialogTitle, resizeable := True ]
    ; p <- panel d []
    ; theListBox <- singleListBox p [ items := strings, selection := 0]
    ; ifJust initialSelection $ \selString ->
        case elemIndex selString strings of
            Nothing -> internalError "CommonIO" "singleSelectionDialog"
                            (  "initial selection " ++ show selString
                            ++ " can not be found in " ++ show strings )
            Just i -> set theListBox [ selection := i ]
    ; ok    <- button p [text := "Ok"]
    ; can   <- button p [text := "Cancel", identity := wxID_CANCEL]
    ; buttonSetDefault ok
    ; set d [ layout := container p $
                        column 10 [ vfill $ widget theListBox
                                  , row 5 [widget ok, widget can]
                                  ]
            , clientSize := sz 300 400
            ]
    ; showModal d $ \stop ->
                do set ok  [on command := safetyNet parentWindow $
                                          do index <- get theListBox selection
                                             stop (Just (safeIndex "CommonIO.singleSelectionDialog" strings index))]
                   set can [on command := safetyNet parentWindow $
                                          stop Nothing]
    }

-- | Fill a grid from a list of lists of texts. Each list inside the
--   big list represents a row. Also set the given number or rows and
--   columns to be header: grey background and not editable.
--   This function assumes that the normal spreadsheet-like grid header row
--   and column have been made invisible.
fillGridFromList :: Grid () -> Int -> Int -> [[String]] -> IO ()
fillGridFromList _ _ _ [] = return ()
fillGridFromList theGrid nrHeaderRows nrHeaderCols list =
  do{ nrOfCols <- gridGetNumberCols theGrid
    ; nrOfRows <- gridGetNumberRows theGrid
    ; when (length list > nrOfRows || maximum (map length list) > nrOfCols) $
        internalError "Common" "fillGridFromList" "grid is not big enough"
    ; sequence_ . concat $
        [ [   do{ gridSetCellValue theGrid rowNr colNr txt
                ; let isHeaderCell = rowNr < nrHeaderRows || colNr < nrHeaderCols
                ; gridSetCellBackgroundColour theGrid rowNr colNr
                        (if isHeaderCell then systemGrey else white)
                ; gridSetReadOnly theGrid rowNr colNr isHeaderCell
                }
          | (txt, colNr) <- zip theRow [0..]
          ]
        | (theRow, rowNr) <- zip list [0..]
        ]
    }

-- | Export some data (a list of lists of strings) to a tab delimited
--   file. The user is asked to choose a location
exportToTabFile :: Window a -> String -> String -> [[String]] -> IO ()
exportToTabFile parentWindow description fileName theData =
 do { mFilename <- fileSaveDialog
                       parentWindow
                       False -- remember current directory
                       True  -- overwrite prompt
                       ("Export " ++ description)
                       [("Tab delimited files",["*.txt"])]
                       "" -- directory
                       fileName
    ; ifJust mFilename $ \filename ->
            ignoreResult (safeWriteFile parentWindow filename (tabDelimited theData))
    }

getScreenPPI :: IO Size
getScreenPPI =
  do{ dc <- screenDCCreate
    ; s <- dcGetPPI dc
    ; screenDCDelete dc
    ; return s
    }

screenToLogicalPoint :: Size -> Point -> DoublePoint
screenToLogicalPoint ppi p =
    DoublePoint (screenToLogicalX ppi (pointX p))
                (screenToLogicalY ppi (pointY p))

logicalToScreenPoint :: Size -> DoublePoint -> Point
logicalToScreenPoint ppi doublePoint =
    pt (logicalToScreenX ppi (doublePointX doublePoint))
       (logicalToScreenY ppi (doublePointY doublePoint))

screenToLogicalX :: Size ->  Int -> Double
screenToLogicalX ppi x =
    fromIntegral x / (fromIntegral (sizeW ppi) / 2.54)

logicalToScreenX :: Size -> Double -> Int
logicalToScreenX ppi x =
    truncate (x * fromIntegral (sizeW ppi) / 2.54)

screenToLogicalY :: Size -> Int -> Double
screenToLogicalY ppi y =
    fromIntegral y / (fromIntegral (sizeH ppi) / 2.54)

logicalToScreenY :: Size -> Double -> Int
logicalToScreenY ppi y =
    truncate (y * fromIntegral (sizeH ppi) / 2.54)

-- Create a grid of which the standard labels (A,B,C... for columns
-- and 1,2,3... for rows) are invisible
mkNoLabelGrid :: Window a -> Int -> Int -> IO (Grid ())
mkNoLabelGrid thePanel nrOfRows nrOfCols =
  do{ theGrid <- gridCreate thePanel idAny rectNull 0
    ; gridCreateGrid theGrid nrOfRows nrOfCols 0
    ; gridSetColLabelSize theGrid 0
    ; gridSetRowLabelSize theGrid 0
    ; return theGrid
    }

resizeGrid :: Grid () -> Int -> Int -> IO ()
resizeGrid theGrid nrOfRows nrOfCols =
  do{ oldNrOfRows <- gridGetNumberRows theGrid
    ; oldNrOfCols <- gridGetNumberCols theGrid
    ; when (nrOfRows > oldNrOfRows) . ignoreResult $
        gridAppendRows theGrid (nrOfRows - oldNrOfRows) False
    ; when (nrOfRows < oldNrOfRows) . ignoreResult $
        gridDeleteRows theGrid nrOfRows (oldNrOfRows - nrOfRows) False
    ; when (nrOfCols > oldNrOfCols) . ignoreResult $
        gridAppendCols theGrid (nrOfCols - oldNrOfCols) False
    ; when (nrOfCols < oldNrOfCols) . ignoreResult $
        gridDeleteCols theGrid nrOfCols (oldNrOfCols - nrOfCols) False
    }

-- | Get the position of a frame, if the frame is minimized or maximized
--   it is restored to its normal size first. Otherwise, you get
--   (-32000, -32000) for a minimized window :-)
safeGetPosition :: Frame a -> IO (Int, Int)
safeGetPosition f =
  do{ isMax <- frameIsMaximized f
    ; isMin <- frameIsIconized  f
    ; when (isMax || isMin) $ frameRestore f
    ; p <- get f position
    ; return (pointX p, pointY p)
    }

-- Show a dialog with a grid and a save button
gridDialogWithSave :: Window a -> String -> Maybe String -> [[String]]
                   -> IO () -> IO ()
gridDialogWithSave parentWindow title maybeNote matrixContents saveAction =
  do{
    -- Create dialog and panel
    ; theDialog <- dialog parentWindow
        [ text := title
        , resizeable := True
        ]
    ; p <- panel theDialog []

    -- Create and fill grid
    ; theGrid <- mkNoLabelGrid p height width
    ; gridEnableEditing theGrid False
    ; fillGridFromList theGrid 0 0 matrixContents
    ; gridAutoSizeColumns theGrid False

    -- File menu
    ; saveButton <- button p
        [ text := "Save as..."
        , on command := safetyNet parentWindow $ saveAction
        ]

    -- Dialog layout
    ; set theDialog
        [ layout := minsize (sz 600 400) $ column 5
                    ( case maybeNote of
                        Just note -> [ hfill $ label note ]
                        Nothing   -> []
                    ++ [ container p $
                            column 5 [ fill $ widget theGrid
                                     , row 0 [ widget saveButton, glue ]
                                     ]
                       ]
                    )
        , visible := True
        ]
    }
 where
    width  = maximum . map length $ matrixContents
    height = length matrixContents


-- | Using bootstrapUI, a record containing all widgets and variables can be created
-- at the end of the create function, but still referred to before creation
-- NOTE: widgets should not be referred to in a strict way because this will
-- cause a loop
bootstrapUI :: (uistate -> IO uistate) -> IO ()
bootstrapUI fIO =
 do { fixIO fIO
    ; return ()
    }