// How to import library functions
import scala.io.StdIn.{readLine,readInt}
import scala.math._
import scala.collection.mutable.ArrayBuffer
import java.io.PrintWriter
import scala.io.Source

/*
You can execute commands directly in the terminal
REPL : Read Evaluate Print Loop
Type scala in terminal to start and :q to quit
*/

// Creates an automatic variable of the correct type
10 + 3 * 5 / 2 //int 

// You can use that variable in the code that follows
"Your answer " + res0

// Define your own variable // This is muttable variable
var myName = "Tahir"

// You can define the type // This is a immutable variable 
val lastName: String = "M."

// ---------- DATA TYPES ----------

// All datatypes in Scala are objects and they include
// (Get the max value with MaxValue)
// Byte : -128 to 127
// Boolean : true or false
// Char : unsigned max value 65535
// Short : -32768 to 32767
// Int : -2147483648 to 2147483647
// Long : -9223372036854775808 to 9223372036854775807
// Float : -3.4028235E38 to 3.4028235E38
// Double : -1.7976931348623157E308 to 1.7976931348623157E308

// A Double will only hold precision up to 15 digits
val num13 = 1.999999999999999

// Create a BigInt
val lgPrime = BigInt("622288097498926496141095869268883999563096063592498055290461")
lgPrime + 1

// random integer
var randInt = 100000

// ---------- MATH ----------
"5 + 4 = " + (5 + 4)
"5 - 4 = " + (5 - 4)
"5 * 4 = " + (5 * 4)
"5 / 4 = " + (5 / 4)
"5 % 4 = " + (5 % 4)

// Shorthand notation (No randInt++, or randInt--)
randInt += 1
"randInt += 1" + randInt

randInt -= 1
"randInt -= 1" + randInt

randInt *= 1
"randInt *= 1" + randInt

randInt /= 1
"randInt /= 1" + randInt

// Import the math library in the terminal import scala.math._

abs(-8)
cbrt(64) // Cube root a^3 = n (Find a)
ceil(5.45)
round(5.45)
floor(5.45)
exp(1) // Euler's number raised to the power
pow(2, 2) // 2^2
sqrt(pow(2,2) + pow(2,2))
hypot(2, 2) // sqrt(X^2 + y^2)
log10(1000) // = 3 (10 × 10 × 10 = 10^3)
log(2.7182818284590455) // Natural logarithm to the base e
min(5, 10)
max(5, 10)
(random * (11 - 1) + 1).toInt // Random number between 1 and 10
toRadians(90)
toDegrees(1.5707963267948966)

// ---------- CONDITIONALS ----------
// if statements are like Java except they return a value like the
// ternary operator

// Conditional Operators : ==, !=, >, <, >=, <=
// Logical Operators : &&, ||, !

var age = 18

val canVote = if (age >= 18) "yes" else "no"

// You have to use { } in the REPL, but not otherwise
if ((age >= 5) && (age <= 6)) {
  println("Go to Kindergarten")
} else if ((age > 6) && (age <= 7)) {
  println("Go to Grade 1")
} else {
  println("Go to Grade " + (age - 5))
}
// True or False
true || false
!(true)

// ---------- STRINGS, ARRAYS AND LISTS ----------
// An immutable sequence of characters, numbers, etc 
val str1: String = "Hello World" 
println(str1)

// Formating a string
val num1 = '10'
val num2 = '20'
val num3 = '30'
val result = printf("(%d -- %f -- %s)", num1, num2, num3) 
// Can also use .format() method instead 
println("(%d -- %f -- %s)".format(num1, num2, num3)) 

// ARRAYS - Can store fixed size of same data type in Scala 
// Initialize an array of fixed size and type 
val myarray: Array[Int] = new Array[Int](4); 
val myarray2 = new Array[Int](5); 
var myarray = Array(1,2,3,4,5,6)

// Assign values at index 
myarray[0] = 10

// Traversing thru an array 
for (x <- myarray) {
	print(x)
}

// Another way 
for (i <- 0 to (myarray.length - 1)) { 
	println(myarray(i));
) 

// Concatnate two arrays 
import Array._ 
val result = concat(myarray, myarray3); 

// LISTS -- Similar to array, must be holding same data types 
// Arrays are NOT immutable, whereas Lists are immutable 
// I.e. You cannot change the values of Lists once its assigned! 
val mylist: List[Int] = List(1,2,5,6,9,6,4);
val names: List[String] = List("Max", "Tahir", "John") 

// Can also work as a Linked list 
println(names.head) // the first value of the list 
println(names.tail) // W.e. you get after removing the first value
println(names.isEmpty) // Check if it's empty
println(names.reverse) // Reverse the list 

// ---------- FUNCTIONS ----------
// First, learn about Objects vs Classes 
// Objects are initiantized values of classes, i.e. instead of doing class{ ... }, 
// and then intiantizing a value from it such as person1 = Class(Person), and then calling 
// it's variables / methods, i.e. person1.say_hello(), objects are already intiantized
// So can already do Object.method(). 
object Demo{

	object Math{

		def add(x: Int, y: Int): Int = {
				return x + y; 
		}
	}
}
// All of these would work
print(Math.add(5,10))
println(Math.add(5,10))

// Function syntax 
def sum(x: Int, y: Int): Int ={
  return x+y; 
}
val total = sum(2,3)
// Driver Code
println(total)

// Can also provide default parameters
def sum(x: Int = 10, y: Int = 20): Int ={
  return x+y; 
}

// Function that doesn't return 
// This won't return anything due to the Unit.
// Unit is same as Void in some other languages
def print(x: Int, y: Int): Unit ={
	println(x+y);
}

// Functions can be assigned to variables! 
var add = (x: Int, y: Int) => x + y; 
println(add(300,500)); 

// Higher order Functions in Scala
// Take functions as arguments, and return fucntions as a result 

// Partially Applied Functions
// The above just means you apply some arguments, and leave one blank to change around. 
impoort java.util.Date 
def log(date: Date, message: String) = {
	println(date + " " + message);
}
val date = new Date;
val newLog = log(date, _ :String);
newLog("The message 1");  // Only need to pass the missing parameters ! 

// Scala Closures 
// Function which uses variables from OUTSIDE this function
// (Global variables) 

// Closure will take the most recent value of the number
var number = 10; 
val add = (x: Int) => x + number; 

// Function currying -> A function that has multiple arguments is written in a way such that it only takes 1 argument)
def add(x: Int, y: Int) = x + y; 
def add2(x:Int) = (y: Int) => x+y; 
println(add(20,10));
println(add2(20)(10)); 

// We can also use it with variables 
val sum40 = add2(40);  // This will be the first value 
print(sum40(100)) // Now next time when we call it with another parameter, that'll get read as the second param
/// This will now be 140! /
println(sum40(50)) // This will be 90 

// ------- HASHMAP, MAP, FLATMAP, FLATTEN AND FILTER (HIGH ORDER METHODS) ----------
// Maps -> Collection of Key-Value pairs, where the keys are unique
// Same thing as a Hashmap -- Duplication of keys is not possible 
val myMap: Map[Int, String] = 
	Map(801 -> "max", 802 -> "Tom", 804 -> "July"); 

print(mymap);
println(mymap(802)); // Will print tom