{-|
Module: Sudoku.Solver
Description : Provides functions to solve a sudoku.
Copyright: (c) Marcel Moosbrugger, 2017
License     : MIT

This module provides functions for solving a sudoku.
-}
module Sudoku.Solver (solveSudoku) where

import           Sudoku.Type
import           Util

-- Internal data types
-- -------------------

-- | Internal type representing a two dimensional matrix.
type Matrix a = [[a]]
-- | Internal type representing a sudoku board.
type Board    = Matrix Char
-- | Internal type represinting the choices for a single field in the sudoku.
type Choices  = [Char]


-- Helper functions
-- ----------------

-- | Is true iff a character represents an empty field.
blank :: Char -> Bool
blank = (==) blankval

-- | Splits a list into multiple list of length boxsize.
group :: [a] -> [[a]]
group = groupBy boxsize

-- Selection functions
-- -------------------

-- | Converts a matrix arranged in rows to a matrix arranged in rows.
rows :: Matrix a -> Matrix a
rows = id

-- | Converts a matrix arranged in rows to a matrix arranged in columns.
cols :: Matrix a -> Matrix a
cols [xs]       = [[x] | x <- xs]
cols (xs : xss) = zipWith (:) xs (cols xss)

-- | Converts a matrix arranged in rows to a matrix arranged in boxes.
boxs :: Matrix a -> Matrix a
boxs = map ungroup . ungroup . map cols . group . map group


-- Functions for generating choices and pruning
-- -------------------------------------------

-- | For a given board associates all possible (really all possible values)
--   with every blank field.
choices :: Board -> Matrix Choices
choices = map (map choose)
    where choose e = if blank e then cellvals else [e]

-- |For a list of choices returns those choices which are fixed
-- i.e. the choices which leave only a single choice
fixed :: [Choices] -> Choices
fixed = concat . filter single

-- |Reduces every choices in a list by the fixed choices
-- Imagine the parameter being the choices for every field in a single row
-- The function removes the fixed values from the choices of every field in the row
reduce :: [Choices] -> [Choices]
reduce css = map (remove (fixed css)) css
    where remove fs cs = if single cs then cs else delete fs cs

-- | Applies reduce to every unit in the matrix. Regarding to which units the
--   matrix gets reduced depends on the first parameter with which the matrix can
--   get transformed.
pruneBy :: (Matrix Choices -> Matrix Choices) -> (Matrix Choices -> Matrix Choices)
pruneBy f = f . map reduce . f

-- | Prunes a matrix by pruning it by rows, columns and boxes.
prune :: Matrix Choices -> Matrix Choices
prune = pruneBy boxs . pruneBy cols . pruneBy rows


-- Validity functions for choices
-- ------------------------------

-- | Is true if any field in the matrix has no choices
--   That would mean the resulting sudoku has no solution.
void :: Matrix Choices -> Bool
void = any (any null)

-- | Is true if no unit in the matrix as duplicated fixed elements.
--   That would mean the the resulting sudoku sultion is invalid.
safe :: Matrix Choices -> Bool
safe cm = all (nodups . fixed) (rows cm) &&
          all (nodups . fixed) (cols cm) &&
          all (nodups . fixed) (boxs cm)

-- | Is true if a matrix of choices is either void or not safe
--   That would mean that with the passed matrix of choices no solution can be
--   found anymore.
blocked :: Matrix Choices -> Bool
blocked cm = void cm || not (safe cm)


-- Functions for expanding and searching for a solution
-- ----------------------------------------------------

-- | Is the minimum number of choices a not fixed field in the matrix has
minchoice :: Matrix Choices -> Int
minchoice = minimum . filter (> 1). concat . map (map length)

-- | Out of a given matrix it creates a list of matrices by fixating every
--   possible choice for one field. The field for which the choices get fixated is
--   the first field with the minimum number of choices.
expand :: Matrix Choices -> [Matrix Choices]
expand cm = [rows1 ++ [row1 ++ [c]:row2]  ++ rows2 | c <- cs]
    where (rows1, row:rows2) = break (any best) cm
          (row1, cs:row2)    = break best row
          best cs            = (length cs == n)
          n                  = minchoice cm

-- | Searches all possible solutions. A solution is a a matrix of choices in which
--   every field has only a single choice. If a matrix is blocked there is no
--   solution. If its already a solution it's the only solution. The non trivial
--   case is handled by expanding the matrix, pruning all children and then
--   recursivly search within them for solutions.
search :: Matrix Choices -> [Matrix Choices]
search cm
    | blocked cm          = []
    | all (all single) cm = [cm]
    | otherwise           = (concat . map (search . prune) . expand) cm

-- | Solving a soduku is now just generating all choices, searching for solutions
--   for this choices and then converting the list of matrices of choices back to
--   a list of matrices of characters (or boards). Pruning the choices before
--   passing them on to the search improves the performance.
solve :: Board -> [Board]
solve = map (map $ map head) . search . prune . choices

-- | Returns a list of all solutions for a given sudoku.
solveSudoku :: Sudoku -> Maybe [Sudoku]
solveSudoku = sequence . map fromString . map concat . solve . groupBy boardsize . toString