/-
Copyright (c) 2022 Clara Löh. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.txt.
Author: Clara Löh.
-/

import tactic                      -- standard proof tactics
import topology.metric_space.basic -- basics on metric spaces
import topology.instances.real     -- ℤ as metric space

open classical  -- we work in classical logic

/-
We define quasi-isometries as quasi-isometric embeddings 
that admit a quasi-inverse quasi-isometric embedding. 
We then prove that a quasi-isometric embedding is a 
quasi-isometry if and only if it has quasi-dense image.
-/

/-
# Quasi-isometric embeddings and quasi-isometries
-/

-- quasi-isometric embeddings
def is_QIE_lower
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c b : ℝ)
:= ∀ x x' : X, dist (f x) (f x') ≥ 1/c * dist x x' - b    

def is_QIE_upper
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c b : ℝ)
:= ∀ x x' : X, dist (f x) (f x') ≤ c * dist x x' + b    

def is_QIE' 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c b : ℝ)
:= is_QIE_upper f c b 
 ∧ is_QIE_lower f c b 

def is_QIE 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
:= ∃ c : ℝ, ∃ b : ℝ,
   c > 0 
 ∧ b > 0
 ∧ is_QIE' f c b 


-- finite distance
def has_fin_dist' 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f g : X → Y)
    (c : ℝ)
:= ∀ x : X, dist (f x) (g x) ≤ c    

def has_fin_dist 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f g : X → Y)
:= ∃ c : ℝ, 
   c > 0
 ∧ has_fin_dist' f g c

def are_quasi_inverse 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (g : Y → X)
:= has_fin_dist (g ∘ f) id
 ∧ has_fin_dist (f ∘ g) id 

-- quasi-isometry
def is_QI 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
:= is_QIE f
 ∧ ∃ g : Y → X, is_QIE g 
              ∧ are_quasi_inverse f g

/-
# Two lemmas on quasi-isometric embeddings
-/

-- rewriting the lower estimate for quasi-isometric embeddings
lemma QIE_lower_est 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c b : ℝ)
    (c_pos : c > 0)
    (f_is_QIE : is_QIE' f c b)
  : ∀ x x' : X, dist x x' ≤ c * dist (f x) (f x') + c * b 
:=
begin 
  have c_neq_0 : c ≠ 0, 
       by exact ne_of_gt c_pos,
  have nonneg_c : 0 ≤ c,
       by exact le_of_lt c_pos,

  assume x x' : X,

  have lower_est : 1/c * dist x x' - b ≤ dist (f x) (f x'), 
       by exact f_is_QIE.2 x x',

  calc dist x x' 
         = c * 1/c * dist x x' - c * b + c * b 
         : by simp[div_self,c_neq_0]
     ... = c * (1/c * dist x x' - b) + c * b 
         : by ring
     ... ≤ c * dist (f x) (f x') + c * b 
         : by {apply add_le_add_right, 
               exact mul_le_mul_of_nonneg_left lower_est nonneg_c},
end  

-- Sometimes, it is convenient to be able to use 
-- different constants for the upper/lower estimates
lemma QIE_from_different_constants
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c1 b1 c2 b2: ℝ)
    (c1_pos : c1 > 0)
    (b1_pos : b1 > 0)
    (c2_pos : c2 > 0)
    (b2_pos : b2 > 0)
    (f_QIE_upper : is_QIE_upper f c1 b1)
    (f_QIE_lower : is_QIE_lower f c2 b2)
  : is_QIE f 
:=
begin
  unfold is_QIE,
  unfold is_QIE',

  -- we increase the given constants suitably:
  let c := c1 + c2,
  let b := b1 + b2,

  use c,
  use b,

  -- preparation: basic estimates for the constants:
  have c_pos : c > 0, 
       by exact add_pos c1_pos c2_pos,
  have nonneg_c : 0 ≤ c, 
       by exact le_of_lt c_pos,
  have b_pos : b > 0,
       by exact add_pos b1_pos b2_pos,
  have c1_leq_c : c1 ≤ c, 
       by simp[le_of_lt c2_pos],
  have b1_leq_b : b1 ≤ b, 
       by simp[le_of_lt b2_pos],
  have b2_leq_b : -b2 ≥ -b,
       by simp[le_of_lt b1_pos],
  have c2_leq_c' : c2 ≤ c, 
       by simp[le_of_lt c1_pos],     
  have c2_leq_c : 1/c2 ≥ 1/c,
       by simp[c2_leq_c', inv_le_inv_of_le c2_pos],

  -- Now, the upper/lower estimates are basic calculations:
  have f_QIE_upper_cb : is_QIE_upper f c b, by 
  begin 
    unfold is_QIE_upper,
    assume x x' : X,
    calc dist (f x) (f x')
           ≤ c1 * dist x x' + b1 
           : by exact f_QIE_upper x x' 
       ... ≤ c1 * dist x x' + b 
           : by exact add_le_add_left b1_leq_b (c1 * dist x x')
       ... ≤ c * dist x x' + b 
           : by {apply add_le_add_right,
                 exact mul_le_mul_of_nonneg_right c1_leq_c dist_nonneg},
  end,

  have f_QIE_lower_cb : is_QIE_lower f c b, by 
  begin 
    unfold is_QIE_lower,
    assume x x' : X,
    calc dist (f x) (f x')
           ≥ 1/c2 * dist x x' - b2 
           : by exact f_QIE_lower x x' 
       ... ≥ 1/c2 * dist x x' - b 
           : by exact add_le_add_left b2_leq_b _
       ... ≥ 1/c * dist x x' - b
           : by {apply add_le_add_right, 
                 exact mul_le_mul_of_nonneg_right c2_leq_c dist_nonneg}, 
  end,

  show _, 
       by exact ⟨ c_pos, b_pos, 
                  ⟨ f_QIE_upper_cb, f_QIE_lower_cb ⟩⟩,
end     


/-
# An alternative characterisation of quasi-isometries
-/

-- We show that a quasi-isometric embedding is a quasi-isometry 
-- if and only if it has quasi-dense image.
-- We show the two implications separately: 

def has_quasidense_image'
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
    (c : ℝ)
:= ∀ y : Y, ∃ x : X, dist (f x) y ≤ c    

def has_quasidense_image 
    {X Y : Type*} [metric_space X] [metric_space Y]
    (f : X → Y)
:= ∃ c : ℝ, 
   c > 0 
 ∧ has_quasidense_image' f c

-- Quasi-isometries have quasi-dense image:
theorem QI_has_quasidense_image
     {X Y : Type*} [metric_space X] [metric_space Y]
     (f : X → Y)
     (f_is_QI : is_QI f)
   : has_quasidense_image f 
:= 
begin 
  have ex_qinv : ∃ g : Y → X, is_QIE g 
                            ∧ are_quasi_inverse f g, 
       by exact f_is_QI.2,
  rcases ex_qinv with ⟨ g, ⟨ is_QIE_g, fg_qinv ⟩⟩,

  have fg_close_to_id : ∃ c : ℝ, c > 0 ∧ has_fin_dist' (f ∘ g) id c, 
       by exact fg_qinv.2,
  rcases fg_close_to_id with ⟨ c, c_pos, fg_c_close_to_id ⟩,
  
  -- This constant c is a witness for the quasi-density of the image:
  use c,
  split,
  show c > 0, 
       by exact c_pos,

  show has_quasidense_image' f c, by 
  begin 
    unfold has_quasidense_image',
    assume y,
    let x := g y,
    use x,
    show dist (f x) y ≤ c, by 
    calc dist (f x) y = dist (f (g y)) y : by simp 
                  ... ≤ c                : by exact fg_c_close_to_id y,
  end,
end

-- Quasi-isometric embeddings with quasi-dense image are quasi-isometries:

-- Preparation: 
-- Quasi-inverses of quasi-isometric embeddings 
-- are quasi-isometric embeddings
lemma quasiinverse_of_QIE_is_QIE 
     {X Y : Type*} [metric_space X] [metric_space Y]
     (f : X → Y)
     (g : Y → X)
     (f_is_QIE : is_QIE f)
     (fg_qinv : are_quasi_inverse f g)
   : is_QIE g
:=
begin
  -- We choose constants witnessing that 
  -- f is a quasi-isometric embedding and that 
  -- f and g are quasi-inverse to each other:
  rcases f_is_QIE with ⟨cf, bf, cf_pos, bf_pos,  
                        f_is_QIE_upper, f_is_QIE_lower⟩,
  rcases fg_qinv with ⟨⟨c_gf, c_gf_pos, gf_close_to_id⟩, 
                       ⟨c_fg, c_fg_pos, fg_close_to_id⟩⟩,
  have f_is_cfbf_QIE : is_QIE' f cf bf,
       by exact ⟨ f_is_QIE_upper, f_is_QIE_lower ⟩, 

  -- We combine these constants appropriately:
  let c1 := cf,
  let b1 := cf * (2 * c_fg + bf),
  let c2 := cf,
  let b2 := 1/cf * (2 * c_fg + bf),
  have c1_pos : c1 > 0, 
       by exact cf_pos,
  have b1_pos : b1 > 0, 
       by {apply mul_pos cf_pos, 
           apply add_pos _ bf_pos, 
           simp[mul_pos _ c_fg_pos]},
  have c2_pos : c2 > 0,
       by exact cf_pos,
  have b2_pos : b2 > 0,
       by {apply mul_pos,
           apply one_div_pos.mpr cf_pos,
           apply add_pos _ bf_pos, 
           simp[mul_pos _ c_fg_pos]},

  -- The upper estimate for g:
  have g_is_QIE_upper : is_QIE_upper g c1 b1, by 
  begin 
    unfold is_QIE_upper,
    assume y y' : Y,
    let x  := g y,
    let x' := g y',

    have dist_f_estimate : dist (f x) (f x') ≤ dist y y' + 2 * c_fg, 
    by calc dist (f x) (f x') 
              ≤ dist (f x) y + dist y (f x')
              : by simp[dist_triangle]
          ... ≤ dist (f x) y + dist y y' + dist y' (f x') 
              : by {ring_nf,simp[dist_triangle y y' (f x')]}
          ... ≤ c_fg + dist y y' + dist y' (f x') 
              : by {simp, exact fg_close_to_id y}
          ... ≤ c_fg + dist y y' + c_fg 
              : by {simp,rw[dist_comm],exact fg_close_to_id y'}    
          ... ≤ dist y y' + 2 *c_fg 
              : by ring_nf,

    calc dist (g y) (g y')
           = dist x x' 
           : by  refl 
       ... ≤ cf * dist (f x) (f x') + cf * bf 
           : by exact QIE_lower_est f cf bf cf_pos f_is_cfbf_QIE x x'
       ... ≤ cf * (dist y y' + 2 * c_fg) + cf * bf 
           : by simp[le_of_lt cf_pos,mul_le_mul_of_nonneg_left,
                     dist_f_estimate]
       ... = cf * dist y y' + cf * (2 * c_fg + bf)
           : by ring        
       ... ≤ c1 * dist y y' + b1 
           : by refl,
  end,

  -- The lower estimate for g:
  have g_is_QIE_lower : is_QIE_lower g c2 b2, by 
  begin 
    unfold is_QIE_lower,
    assume y y' : Y,
    let x  := g y,
    let x' := g y',

    have cf_times_claim : 
         cf * dist (g y) (g y') ≥ dist y y' - cf * b2, 
    by calc cf * dist (g y) (g y') 
             = cf * dist x x' 
             : by refl 
         ... ≥ dist (f x) (f x') - bf 
             : by {simp,exact f_is_QIE_upper x x'}
         ... ≥ dist (f x) y' - dist (f x') y' - bf
             : by simp[dist_triangle]
         ... ≥ dist y' y - dist (f x) y - dist (f x') y' - bf 
             : by simp[dist_triangle_left y' y (f x)]
         ... ≥ dist y' y - c_fg - dist (f x') y' - bf 
             : by {simp, exact fg_close_to_id y}
         ... ≥ dist y' y - c_fg - c_fg - bf 
             : by {simp, exact fg_close_to_id y'}
         ... ≥ dist y y' - cf * (1/cf * (2 * c_fg + bf)) 
             : by {simp[ne_of_gt cf_pos,dist_comm], ring_nf}           
         ... = dist y y' - cf * b2
             : by refl,

    have cf_inv_nonneg : 0 ≤ cf⁻¹, 
         by simp[inv_nonneg.mpr (le_of_lt cf_pos)],

    calc dist (g y) (g y')
           = 1/cf * (cf * dist (g y) (g y'))
           : by simp[ne_of_gt cf_pos] 
       ... ≥ 1/cf * (dist y y' - cf * b2)
           : by simp[mul_le_mul_of_nonneg_left cf_times_claim 
                                               cf_inv_nonneg]  
       ... = 1/cf * dist y y' - 1/cf * cf * b2
           : by ring 
       ... = 1/c2 * dist y y' - b2     
           : by simp[ne_of_gt cf_pos],
  end,

  show is_QIE g, 
       by exact QIE_from_different_constants g 
                  c1 b1 c2 b2 
                  c1_pos b1_pos c2_pos b2_pos 
                  g_is_QIE_upper g_is_QIE_lower,
end

theorem QIE_with_quasidense_image_is_QI 
     {X Y : Type*} [metric_space X] [metric_space Y]
     (f : X → Y)
     (f_is_QIE : is_QIE f)
     (f_qdense_im : has_quasidense_image f)
   : is_QI f
:= 
begin 
  -- We obtain a quasi-inverse from the quasi-density of the image 
  -- and the axiom of choice:
  rcases f_qdense_im with ⟨ c, c_pos, f_has_c_dense_im ⟩,  
  rcases classical.axiom_of_choice f_has_c_dense_im
         with ⟨ g, fg_c_close_to_id ⟩,
  -- basic simplifications       
  dsimp at g, 
  dsimp at fg_c_close_to_id,

  -- This candidate indeed is quasi-inverse to f:
  have f_and_g_are_qinv : are_quasi_inverse f g, by 
  begin 
    -- By construction, f ∘ g has finite distance from id  
    have fg_close_to_id : has_fin_dist (f ∘ g) id, by 
    begin 
      unfold has_fin_dist,
      unfold has_fin_dist',
      use c,
      split, 
      show c > 0, 
           by exact c_pos,

      assume y : Y,
      calc dist ((f ∘ g) y) y 
             = dist (f (g y)) y : by refl 
         ... ≤ c                : by exact fg_c_close_to_id y,
    end,

    -- Conversely, also g ∘ f has finite distance from id; 
    have gf_close_to_id : has_fin_dist (g ∘ f) id, by 
    begin 
      unfold has_fin_dist,
      unfold has_fin_dist',
      -- we choose QIE-constants for f ...
      rcases f_is_QIE with ⟨ cf, bf, cf_pos, bf_pos, f_is_cfbf_QIE ⟩,
      -- ... and construct a suitably large constant c':
      let c' := cf * c + cf * bf,
      use c',
      split, 
      show c' > 0, 
           by {apply add_pos, 
               apply mul_pos cf_pos,
               exact c_pos,
               apply mul_pos cf_pos bf_pos},

      assume x,
      show dist ((g ∘ f) x) x ≤ c', by 
      begin 
        let x_fx := g (f x),
        calc dist ((g ∘ f) x) x
               = dist x_fx x 
               : by refl 
           ... ≤ cf * dist (f x_fx) (f x) + cf * bf 
               : by exact QIE_lower_est f cf bf cf_pos f_is_cfbf_QIE x_fx x 
           ... ≤ cf * c + cf * bf 
               : by simp[fg_c_close_to_id (f x), 
                         le_of_lt cf_pos, mul_le_mul_of_nonneg_left]
           ... ≤ c' 
               : by refl,
      end,
    end,

    show are_quasi_inverse f g, 
         by exact ⟨ gf_close_to_id, fg_close_to_id ⟩,
  end,

  -- Hence, g is a quasi-isometric embedding:
  have g_is_QIE : is_QIE g, 
       by exact quasiinverse_of_QIE_is_QIE f g 
                  f_is_QIE f_and_g_are_qinv,

  -- We conclude that f is a quasi-isometry 
  -- by putting everything together:
  show is_QI f, 
       by exact ⟨ f_is_QIE, 
                  begin 
                    use g, 
                    exact ⟨ g_is_QIE, f_and_g_are_qinv⟩ 
                  end⟩,
end    

/-
# An example
-/

-- We use the quasi-density criterion to show that
-- the inclusion of ℤ into ℝ is a quasi-isometry
def i_ZR 
   : ℤ → ℝ 
:= λ x, x

lemma Z_into_R_is_QI 
   : is_QI i_ZR
:= 
begin
  apply QIE_with_quasidense_image_is_QI, 

  show is_QIE i_ZR, by 
  begin 
    unfold is_QIE,
    use 1,
    use 1,

    have one_pos : (1:real) > 0, 
         by simp,

    have i_ZR_is_QIE : is_QIE' i_ZR 1 1, by 
    begin 
      unfold is_QIE',

      have upper_estimate : ∀ x x' : ℤ, 
           dist (i_ZR x) (i_ZR x') ≤ 1 * dist x x' + 1, by
      begin  
        assume x x' : ℤ,
        simp[i_ZR],
      end,

      have lower_estimate : ∀ x x' : ℤ, 
           dist (i_ZR x) (i_ZR x') ≥ 1/1 * dist x x' - 1, by 
      begin 
        assume x x' : ℤ,
        simp[i_ZR],
      end,        

      show _, 
           by {simp only[is_QIE_upper,is_QIE_lower], 
               exact ⟨upper_estimate, lower_estimate ⟩},
    end,

    show _, 
        by exact ⟨ one_pos, ⟨one_pos, i_ZR_is_QIE⟩⟩, 
  end,

  show has_quasidense_image i_ZR, by 
  begin 
    unfold has_quasidense_image,
    use 1, 

    have one_pos : (1:real) > 0, 
         by simp,

    have qdense_im : has_quasidense_image' i_ZR 1, by 
    begin 
      unfold has_quasidense_image',

      assume y : ℝ,
      let x := int.floor y,
      use x,

      show dist (i_ZR x) y ≤ 1, by 
      begin 
        calc dist (i_ZR x) y 
               = dist y (i_ZR x) 
               : by exact dist_comm _ _ 
           ... = |y - ↑x| 
               : by refl
           ... = y - ↑x
               : by simp[int.floor_le,int.fract_nonneg]
           ... ≤ 1 
               : by simp[int.fract_lt_one,le_of_lt],
      end,
    end,

    show _, 
         by exact ⟨one_pos, qdense_im⟩,
  end,
end   

