module HaskellMasterSolutions where

import Data.Char (isDigit, isLetter, isLower, isUpper)

-- Haskell 1

-- Ex. 1
-- Define a function that returns a greeting for a given name.
hello :: String -> String
hello name = "Hello " ++ name ++ "!!!"

-- Ex. 2
-- Define a function that calculates the volume of a box.
volume :: Num a => a -> a -> a -> a
volume x y z = x * y * z

-- Ex. 3a
-- Define a function that doubles a number.
doubleMe :: Num a => a -> a
doubleMe x = 2 * x

-- Ex. 3b
-- Define the list [11..99].
myNumbers :: [Int]
myNumbers = [11 .. 99]

-- Ex. 3c
-- Apply doubleMe to every element of myNumbers.
doubledMyNumbers :: [Int]
doubledMyNumbers = map doubleMe myNumbers

-- Ex. 3d
-- Keep only numbers divisible by 13.
mod13 :: Integral a => [a] -> [a]
mod13 = filter (\x -> x `mod` 13 == 0)

-- Ex. 4
-- Build initials from first and last name, with dots.
initial :: String -> String -> String
initial firstName lastName = take 1 firstName ++ "." ++ take 1 lastName ++ "."

-- Ex. 5
-- Build the unit matrix of degree n.
unitaryN :: Int -> [[Int]]
unitaryN n =
  [ [if row == col then 1 else 0 | col <- [1 .. n]]
  | row <- [1 .. n]
  ]

-- Ex. 6
-- List all three-digit numbers divisible by 3 and having distinct digits.
threeDigitDivBy3Distinct :: [Int]
threeDigitDivBy3Distinct =
  [ number
  | a <- [1 .. 9]
  , b <- [0 .. 9]
  , c <- [0 .. 9]
  , let number = 100 * a + 10 * b + c
  , number `mod` 3 == 0
  , a /= b
  , a /= c
  , b /= c
  ]

-- Ex. 7a
-- Keep only lowercase letters from a string.
onlyLowercase :: String -> String
onlyLowercase = filter isLower

-- Ex. 7b
-- Keep only uppercase letters from a string.
onlyUppercase :: String -> String
onlyUppercase = filter isUpper

-- Ex. 7c
-- Keep all characters that are not digits.
allNotDigits :: String -> String
allNotDigits = filter (not . isDigit)

-- Haskell 2

-- Uses 1-based indexing: charN 1 "Anna" == 'A'
-- Ex. 1
-- Return the nth character of a string.
charN :: Int -> String -> Char
charN n s
  | n <= 0 = error "Index must be positive"
  | n > length s = error "Index out of range"
  | otherwise = s !! (n - 1)

-- Safe helper version of charN using Maybe.
charNMaybe :: Int -> String -> Maybe Char
charNMaybe n s
  | n <= 0 = Nothing
  | n > length s = Nothing
  | otherwise = Just (s !! (n - 1))

-- Ex. 2
-- Sample list of six pairs: (name, surname).
listA :: [(String, String)]
listA =
  [ ("Anna", "Lis")
  , ("Marek", "Nowak")
  , ("Ola", "Kowalska")
  , ("Jan", "Mazur")
  , ("Ewa", "Kaczmarek")
  , ("Piotr", "Lewandowski")
  ]

-- Ex. 2
-- Two-digit numbers with tens digit 3, 5 or 7 and units digit 3 or 4.
listB :: [Int]
listB = [10 * tens + ones | tens <- [3, 5, 7], ones <- [3, 4]]

-- Ex. 2a
-- Extract only names from listA.
listA1 :: [String]
listA1 = map fst listA

-- Ex. 2b
-- Extract only surnames from listA.
listA2 :: [String]
listA2 = map snd listA

-- Ex. 2c
-- Build all triples (name, surname, age).
people :: [(String, String, Int)]
people = [(name, surname, age) | (name, surname) <- listA, age <- listB]

-- Ex. 3
-- Check whether a number is prime.
isPrime :: Int -> Bool
isPrime n
  | n < 2 = False
  | otherwise = null [d | d <- [2 .. integerSqrt n], n `mod` d == 0]

-- Helper: integer square root bound for prime checking.
integerSqrt :: Int -> Int
integerSqrt = floor . sqrt . fromIntegral

-- Ex. 3
-- Return the first n prime numbers.
nPrimes :: Int -> [Int]
nPrimes n = take n [x | x <- [2 ..], isPrime x]

-- Ex. 4
-- Return (first element of first list, last element of second list),
-- or (0,0) if one of the lists is empty.
pair :: Num a => [a] -> [a] -> (a, a)
pair (x : _) ys@(_ : _) = (x, last ys)
pair _ _ = (0, 0)

-- Ex. 5a
-- Sign function written with if ... then ... else.
signIf :: (Ord a, Num a) => a -> a
signIf x =
  if x > 0
    then 1
    else
      if x < 0
        then -1
        else 0

-- Ex. 5b
-- Sign function written with guards.
signGuards :: (Ord a, Num a) => a -> a
signGuards x
  | x > 0 = 1
  | x < 0 = -1
  | otherwise = 0

-- Ex. 6a
-- Surface area of a cuboid using where.
cuboidSurfaceWhere :: Num a => a -> a -> a -> a
cuboidSurfaceWhere x y z = 2 * base + 2 * front + 2 * side
  where
    base = x * y
    front = x * z
    side = y * z

-- Ex. 6b
-- Surface area of a cuboid using let ... in.
cuboidSurfaceLet :: Num a => a -> a -> a -> a
cuboidSurfaceLet x y z =
  let base = x * y
      front = x * z
      side = y * z
   in 2 * base + 2 * front + 2 * side

-- Haskell 3

-- Ex. 1
-- Infinite sequence defined by c1 = 1, cn = 1 + 2*c(n-1).
cSequence :: [Int]
cSequence = iterate (\x -> 1 + 2 * x) 1

-- Ex. 1a
-- The 15th element of sequence c.
c15 :: Int
c15 = cSequence !! 14

-- Ex. 1b
-- First 15 elements of sequence c.
first15C :: [Int]
first15C = take 15 cSequence

-- Ex. 1c
-- First n elements of sequence c.
firstNC :: Int -> [Int]
firstNC n = take n cSequence

-- Ex. 1d
-- Elements of sequence c that are less than 1000.
cBelow1000 :: [Int]
cBelow1000 = takeWhile (< 1000) cSequence

-- Ex. 2
-- Infinite list of pairs (a_n, b_n) generated from the recurrence.
abPairs :: [(Integer, Integer)]
abPairs = iterate nextPair (1, 1)
  where
    nextPair (a, b) = (a + b, a * b)

-- Ex. 2
-- Sequence a_n extracted from abPairs.
aSequence :: [Integer]
aSequence = map fst abPairs

-- Ex. 2
-- Sequence b_n extracted from abPairs.
bSequence :: [Integer]
bSequence = map snd abPairs

-- Ex. 2
-- First 10 elements of sequence a_n.
first10A :: [Integer]
first10A = take 10 aSequence

-- Ex. 2
-- First 10 elements of sequence b_n.
first10B :: [Integer]
first10B = take 10 bSequence

-- Ex. 3
-- Return the first element of a list.
firstFromList :: [a] -> a
firstFromList (x : _) = x
firstFromList [] = error "Empty list"

-- Ex. 3
-- Return the second element of a list.
secondFromList :: [a] -> a
secondFromList (_ : y : _) = y
secondFromList _ = error "List has fewer than two elements"

-- Ex. 3
-- Return the last element of a list.
lastFromList :: [a] -> a
lastFromList [x] = x
lastFromList (_ : xs) = lastFromList xs
lastFromList [] = error "Empty list"

-- Ex. 3
-- Return the element before the last element of a list.
prelastFromList :: [a] -> a
prelastFromList [x, _] = x
prelastFromList (_ : xs) = prelastFromList xs
prelastFromList _ = error "List has fewer than two elements"

-- Ex. 4
-- Merge a list of lists into one list.
mergeLists :: [[a]] -> [a]
mergeLists = foldr (++) []

-- Ex. 5
-- Count how many times an element appears in a list.
howManyTimes :: (Num a, Eq t) => t -> [t] -> a
howManyTimes value = fromIntegral . length . filter (== value)

-- Ex. 6
-- Take elements from two lists alternately, stopping when one list ends.
connectOneAfterOne :: [a] -> [a] -> [a]
connectOneAfterOne (x : xs) (y : ys) = x : y : connectOneAfterOne xs ys
connectOneAfterOne _ _ = []

-- Haskell 4

-- Ex. 1
-- Sum only positive numbers from a list.
sumPositiveOnList :: [Int] -> Int
sumPositiveOnList = sum . filter (> 0)

-- Ex. 2
-- Count how many elements satisfy a predicate.
count :: Num p => (t -> Bool) -> [t] -> p
count predicate = fromIntegral . length . filter predicate

-- Ex. 3a
-- Check whether a list is a palindrome using built-in reverse.
isPalindromeBuiltIn :: Eq a => [a] -> Bool
isPalindromeBuiltIn xs = xs == reverse xs

-- Helper for Ex. 3b: recursive reverse.
reverseRec :: [a] -> [a]
reverseRec [] = []
reverseRec (x : xs) = reverseRec xs ++ [x]

-- Ex. 3b
-- Check whether a list is a palindrome using a recursive reverse.
isPalindromeRecursive :: Eq a => [a] -> Bool
isPalindromeRecursive xs = xs == reverseRec xs

-- Ex. 4
-- Read two numbers and display their sum and difference.
readTwoNumbersAndShowSumDiff :: IO ()
readTwoNumbersAndShowSumDiff = do
  putStrLn "Enter the first number:"
  x <- (readLn :: IO Double)
  putStrLn "Enter the second number:"
  y <- (readLn :: IO Double)
  putStrLn ("Sum: " ++ show (x + y))
  putStrLn ("Difference: " ++ show (x - y))

-- Ex. 5
-- Read a string and an integer n, then print the string n times,
-- or show an error message for negative n.
repeatStringNTimesIO :: IO ()
repeatStringNTimesIO = do
  putStrLn "Enter a string:"
  s <- getLine
  putStrLn "Enter n:"
  n <- (readLn :: IO Int)
  if n < 0
    then putStrLn "Invalid value n"
    else mapM_ putStrLn (replicate n s)

-- Haskell 5

-- Ex. 1
-- Display the contents of a file on the screen.
displayFile :: FilePath -> IO ()
displayFile path = readFile path >>= putStr

-- Ex. 2
-- Read user text and write it to a file.
writeUserTextToFile :: FilePath -> IO ()
writeUserTextToFile path = do
  putStrLn "Enter text. Finish with an empty line:"
  contentLines <- readTextUntilBlankLine
  writeFile path (unlines contentLines)

-- Helper for Ex. 2: read lines until an empty line is entered.
readTextUntilBlankLine :: IO [String]
readTextUntilBlankLine = do
  line <- getLine
  if null line
    then return []
    else do
      rest <- readTextUntilBlankLine
      return (line : rest)

-- Ex. 3
-- Read a file, reverse the order of its lines, and save to another file.
reverseLinesToNewFile :: FilePath -> FilePath -> IO ()
reverseLinesToNewFile sourcePath targetPath = do
  content <- readFile sourcePath
  writeFile targetPath (unlines (reverse (lines content)))

-- Ex. 4
-- Compute file statistics:
-- number of lines, words, letters, and non-space characters.
fileStatsPure :: String -> (Int, Int, Int, Int)
fileStatsPure content =
  ( length (lines content)
  , length (words content)
  , length (filter isLetter content)
  , length (filter (/= ' ') content)
  )

-- Ex. 4
-- Read a file and print its statistics in a readable form.
fileStats :: FilePath -> IO ()
fileStats path = do
  content <- readFile path
  let (lineCount, wordCount, letterCount, nonSpaceCount) = fileStatsPure content
  putStrLn ("Lines: " ++ show lineCount)
  putStrLn ("Words: " ++ show wordCount)
  putStrLn ("Letters: " ++ show letterCount)
  putStrLn ("Non-space characters: " ++ show nonSpaceCount)
