fn take_ownership(target: String) {
    println!("Ownership of '{target}' has been taken.");
}
 
fn take_ownership_and_return(target: String) -> String {
    println!("Ownership of '{target}' has been taken.");
    println!("Returning ownership ...");
    return target;
}
 
fn main() {
 
    // These are essentially primitve types and they implement the Copy trait
    // Here there is no difference between shallow copy and deep copy.
    let x = 69; // Fixed size, stored on stack
    let y = x;  // Essentially a copy of x on the stack, so both y and x are valid
 
    // These are complex types and they cannot implement the Copy trait (as below),
    // they implement the drop trait which specify how the memory should be freed.
 
    let mut s = String::from("damn"); // stored on heap, mutable, unkown size (size not fixed at compile time)
    s.push_str(" fat");
 
    // This is a shallow copy, only the ptr, len, capacity of s are copied again in the stack
    // so both the ptr's of n and s point to the same address of the heap. So, when both go
    // out of scope both will try to free up the memory in heap. To solve this problem,
    // below essentially s is moved to n and so s is no longer valid (does not exist anymore)
    // so when n goes out of scope the memory in the heap will be freed up (only once, as we needed).
 
    // let n = s; // essentially std::move(s), s is no longer valid from here on. This is a shallow
    // copy (described above).
 
    // This is a deep copy, ptr len capacity are copied in the stack and the data in the heap is
    // copied as well in the heap. So ptr of n points to a different location in heap that stores
    // the same data as s (this data is a copy of the data of s).
    let n = s.clone(); // both n and s are valid
 
    println!("x = {x}, y = {y}"); // Works
    println!("s = {s}, n = {n}"); // Error, with let n = s
 
    // Similar is the case with functions
    let myStr: String = String::from("fck");
    // take_ownership_and_return(myStr); // myStr is no longer valid after this
    // had myStr would've been of a primitive type such as i32, f64, etc. or a type which
    // implements the copy trait, myStr would've still been valid after this function call.
    
    // Functions can also return ownerships
    let myStr = take_ownership_and_return(myStr);
 
    println!("{myStr}");
}