/-

  First we define a namespace for this lecture. In Lean, a namespace is
  simply a way to group a set of definitions with a common prefix. Any
  definition `foo` made inside namespace `N` will have its name prefixed
  with `N.`, i.e., `N.foo`.

  This comment is written as a "block comment".

-/
namespace intro

-- The `inductive` keyword is the command for defining types in Lean. It is used like
-- `class`, `struct`, `data`, and `type` are in other common programming languages.
-- The choice of inductive may seem strange but will become clearer as we move
-- through the course.
--
-- This comment is written with the single line comment of Lean `--`.

/--

  `bool` defines the standard boolean type with two values `tt` and `ff`. We
  don't use the names `true` and `false` because they are already used for the
  logical `true` and logical `false`.  Later in the course we will discuss this
  distinction in more detail.

  In other languages, this definition might look something like
   `enum bool { TT, FF }`.

  This is an example of a doc string, you can hover over `bool` anywhere in
  the editor (after this definition) to see its doc string.

-/
inductive bool : Type
| tt : bool
| ff : bool

-- We can also define namespaced definitions using a compound name as we do
-- below with `bool.not`.
--
-- Note: Its full name will be `intro.bool.nat` since it is defined in an outer
-- namespace.
--
-- The below definition is given by *pattern matching*. Pattern matching is
-- a more powerful version of the switch statement found in mainstream
-- programming languages.
--
-- Pattern matching is an essential part of many functional programming
-- languages and will likely be used in every program we write in Lean.
--
-- Pattern matching allows you to consider the different *constructor*s of an
-- inductive type. In the below example we define `bool.not` by cases.
--
-- If you want to learn about this topic more in depth please see:
--     https://leanprover.github.io/theorem_proving_in_lean/
-- specifically Section 7 & 8 which discuss inductive types and pattern matching.

/-- `bool.not` negates a boolean. -/
def bool.not (b : bool) : bool :=
match b with
| bool.tt := bool.ff
| bool.ff := bool.tt
end

/-- `bool.and` often written as `&&` -/
def bool.and (b1 : bool) (b2 : bool) : bool :=
match b1 with
| bool.tt := b2
| bool.ff := bool.ff
end

-- Lean is different from most languages, since it allows you to not only
-- define *types*, and *functions*, but also proofs.
--
-- These proofs can be written inline and are no different from other
-- definitions in the language, we will talk more in depth about this later.
--
-- For example we can prove that *for all* values of `b1` and `b2`, `bool.and`
-- is commutative.
--
-- Note: This is a property of the program that can be checked _without_
-- executing any code.
--
-- The ablity to write proofs about your programs is what makes Lean unique, and
-- sometimes referred to as a theorem prover.
lemma bool.and_comm :
∀ (b1 b2 : bool),
    bool.and b1 b2 = bool.and b2 b1 :=
begin

/-
    intros,
      cases b1,
        {cases b2; simp [bool.and]},
        { cases b2,
             { simp [bool.and] },
             { simp [bool.and] } }
-/

    intros; cases b1; cases b2; simp [bool.and]
end

-- We could have also defined `bool.and` equivalently as:
def bool.and' : bool → bool → bool :=
fun b1, fun b2,
match b1 with
| bool.tt := b2
| bool.ff := bool.ff
end

-- We can check the type of a definition using the `#check` command.
#check bool.and
#check bool.and'

-- Observe that both types are the same, this due to the fact that
-- functions in Lean are *curried*. A curried function is a function
-- that takes a single argument at a time and returns another function of
-- arity one. For multiple arguments this means you can apply a single
-- argument to get back a new function, see below for examples.

#check (bool.and bool.tt)
#check (bool.and' bool.tt)

-- As detailed below, there is also sugar to combine currying and pattern matching:

/-- `boo.or` often written as `||` -/
def boo.or (b1 : bool) : bool → bool
| bool.tt := bool.ff
| bool.ff := b1

-- We will discuss Lean's builtin numeric types later on, but for now we
-- will present one possible definition of natural numbers.

/--

  A natural number is either `0` or `(1 + n)` where `n` is some natural
  number.

  We represent this using an inductive type with two constructors, one has
  `0` arguments like we saw with `bool`, while `succ` has a different type
  `nat → nat`.

  The `succ` constructor acts as `(1 + n)`, for example `2` can be
  represented as `nat.succ (nat.succ nat.zero)`.

-/
inductive nat : Type
| zero : nat
| succ : nat → nat

-- We demonstrated pattern matching with the *match expression*
--   (`match x with ... end`) 
-- earlier, but there is another more compact way to define a function
-- by cases.
--
-- Similar to the way we define `inductive` types we can just define a function by
-- cases, we just need the `def` keyword, a name, and a function type (`A -> R`).

/-- Test whether a natural number is zero. -/
def is_zero : nat → bool
| nat.zero := bool.tt
| (nat.succ _) := bool.ff

-- As we demonstrated earlier we can define a proof about our program inline.
-- This lemma states that is_zero should always return true for `nat.zero`.
lemma is_zero_zero :
    is_zero nat.zero = bool.tt :=
by simp [is_zero]

/--

    Addition on naturals `ℕ`. We can define addition by simply peeling off every
    plus one (getting `n - 1`) and recursively computing the addition of
    `(n - 1) + m`.

    In the zero case we return `m` resulting in a chain of additions
    by one `1 + 1 + ... + m`, that is, `nat.succ (nat.succ ... m)`.

-/
def add (m : nat) : nat → nat
| nat.zero := m
| (nat.succ n) := nat.succ (add n)

def add'' : nat -> nat -> nat
| m nat.zero := m
| m (nat.succ n) := nat.succ (add'' m n)

/- Addition by zero on the right is identity -/
lemma add_zero_right_id :
    ∀ n, add n nat.zero = n :=
begin
  intros,
  reflexivity, 
  
  -- simp [add]
end

-- Let's try the to prove the other side.

/- Addition by zero on the left is identity -/
lemma add_zero_left_id_fail :
    ∀ n, add nat.zero n = n :=
begin
    intros,
    induction n,
    { reflexivity },
    { simp [add], rw ih_1 }














/-
    intros,
    cases n,
    { reflexivity },
    { cases a,
        { reflexivity },
        { @tactic.fail unit _ _ "This could take a while ..." }
    }
-/
end

-- We need some other way to show that this property of addition holds.

-- If we remember back to our discrete math, or proof courses, our old
-- friend induction can come to the rescue.
lemma add_zero_left_id :
  forall (n : nat),
    add nat.zero n = n :=
begin
  intro n,
  induction n,
  case nat.zero { reflexivity },
  case nat.succ {
    /-

      Find the left hand side of IHn in the goal, and
      replace it with the right hand side of IHn.

    -/
    dsimp [add],
    rw ih_1,
  }
end

/-- Here is another reasonable definition for `add`.
    Note how it differs from the definition above. -/
def add' : nat → nat → nat
| nat.zero n2 := n2
| (nat.succ m1) n2 := add' m1 (nat.succ n2)

/-- EXERCISE: complete the following proof, see the
    solutions in `solutions` namespace at bottom of file. 
lemma add_succ_succ_add' :
  forall x y,
    add x (nat.succ y) = nat.succ (add x y) :=
begin
  intros,
  simp [add]
end
-/

lemma add_succ_succ_add :
  forall y x,
    add (nat.succ x) y = nat.succ (add x y) :=
begin
  intro,
  induction y; intros,
  { simp [add, add'] },
  { simp [add, add'], rw ih_1 }
end

-- EXERCISE: Complete the following proof.
-- Hint: at some point in the proof, rewrite by `add_succ_succ_add`.
lemma add_add' :
  forall x y,
    add x y = add' x y :=
begin
  intro,
  induction x; intro,
  case nat.zero {
    simp [add, add'],
    rw add_zero_left_id,
  },
  case nat.succ {
    simp [add, add'],
    rw add_succ_succ_add,
    rename y foo,
    rw <- ih_1,
    simp [add], 
  }
end


-- We can also define subtraction by one `n - 1` on natural numbers.

/-- Subtraction by one. -/
def pred_fail : nat → nat
| (nat.succ m) := m
| nat.zero := sorry -- intro sorry

-- We can now define a *parametric* inductive type, this is an inductive type
-- which is *generic*.
--
-- This is similar to generics in other programming languages, and allows to
-- define a generic container type.
--
-- `option t` is a type which allows us to encode nullability, either a value of
-- `option t` is `none` (the null value), or `some v` where `v` is a value of
-- type `t`.
inductive option (t : Type)
| none {} : option -- the `{}` annotation tells Lean to infer the type argument `t` to option
| some : t → option

-- Parameters to inductive types are automatically inferred by Lean, in order to
-- do this Lean needs some amount of context in order to figure out what type
-- `none` has.
def option_int : option int := option.none
#check option_int

-- If we check it without context we can see that Lean leaves a guess for the type `?M` in place
-- of the type argument.
#check (none)

-- In the `some` case, the value `nat.zero` allows Lean to solve `t = nat`.
#check (some nat.zero)

/-- Subtraction by one. -/
def pred : nat → option nat
| (nat.succ m) := option.some m
| (nat.zero) := option.none

#eval (pred nat.zero)
#eval (pred (nat.succ nat.zero))
#eval (pred (nat.succ (nat.succ nat.zero)))
#eval (pred (nat.succ (nat.succ (nat.succ nat.zero))))

--
-- In practice, we often us [option] in places
-- where you might throw an exception in other
-- programming languages.
--
--   How many values of type [option] are there?
--   Well, it depends on what option is being
--   paramaterized over.  [option bool] has 3
--   values:
-- <<
--     None
--     Some true
--     Some false
-- >>
--   [option nat] on the other hand has infinitely
--   many values:
-- <<
--     None
--     Some O
--     Some (S 0)
--     Some (S (S O))
--     Some (S (S (S O)))
--     ...
-- >>
--

-- We can easily prove properties about our code that we would normally try to
-- establish using unit tests, but instead we can check them for *all* executions
-- without needing to run our program.

lemma pred_none :
   forall n,
     pred n = option.none ->
     n = nat.zero :=
begin
  intros,
  cases n,
  { reflexivity },
  { simp [pred] at a,
    contradiction }
end

-- `option` only lets us indicate 0 or 1 results
-- from a function. As we'll study more next lecture,
-- we can encode an arbitrary number of results
-- using `list`.

inductive list (t : Type) : Type
| nil {} : list
| cons : t → list → list

def length {t : Type} : list t → nat
| list.nil := nat.zero
| (list.cons x xs) := nat.succ (length xs)

/- Note: The curly braces `{t : Type}` simply indicate
   that Lean should try to infer the `t` argument.
   In this case, Lean will never have any trouble
   figuring what `t` should be because the type of
   the `l` will always unambiguously indicate what
   `t` ought to be. -/

-- Solutions
namespace solutions

lemma add_succ_succ_add :
  forall y x,
    add (nat.succ x) y = nat.succ (add x y) :=
begin
  intro,
  induction y; intros,
  { simp [add, add'] },
  { simp [add, add'], rw ih_1 }
end

lemma add_add' :
  forall x y,
    add x y = add' x y :=
begin
 intro,
 induction x; intros,
 { simp [add, add'],
   rw add_zero_left_id
 },
  {  simp [add, add'],
     rw add_succ_succ_add,
     rw ← ih_1,
     simp [add],
  }
end

end solutions

end intro
