require_relative 'lambda_block'

class Proc
  def or_exception
    -> (value) {
      begin
        right(self.(value))
      rescue Exception => e
        left(e)
      end
    }
  end
end

module Eithers
  private
  def left(value)
    Either.left(value)
  end

  def right(value)
    Either.right(value)
  end

  def is_left
    predicate(-> (either) { either.is_left? })
  end

  def is_right
    predicate(-> (either) { either.is_right? })
  end

  def as_left
    ->(value) { left(value) }
  end
  
  def as_right
    ->(value) { right(value) }
  end
end

class Either
  include Comparable
  include LambdaBlock

  def self.left(value)
    Left.new(value)
  end

  def self.right(value)
    Right.new(value)
  end

  def flatten
    flat_map(identity)
  end
end

class Left < Either
  def initialize(value)
    @value = value
  end

  def is_left?
    true
  end

  def is_right?
    false
  end

  def left_value
    @value
  end

  def right_value
    raise NoSuchElementException.new
  end

  def map_left(fn=nil, &block)
    assert_funcs(fn, block_given?)
    left(block_given? ? block.call(@value) : fn.(@value))
  end

  def flat_map(fn=nil, &block) # a function which returns an either
    assert_funcs(fn, block_given?)
    self
  end

  def fold(seed, fn_left, fn_right)
    fn_left.(seed, @value)
  end

  def <=>(other)
    @value <=> other.left_value
  end

  def to_s
    "left(#{@value})"
  end
end

class Right < Either
  def initialize(value)
    @value = value
  end

  def is_left?
    false
  end

  def is_right?
    true
  end

  def left_value
    raise NoSuchElementException.new
  end

  def right_value
    @value
  end

  def map(fn=nil, &block)
    assert_funcs(fn, block_given?)
    right(block_given? ? block.call(@value) : fn.(@value))
  end

  def map_left(fn=nil, &block)
    assert_funcs(fn, block_given?)
    self
  end

  def flat_map(fn=nil, &block) # a function which returns an either
    assert_funcs(fn, block_given?)
    block_given? ? block.call(@value) : fn.(@value)
  end

  def fold(seed, fn_left, fn_right)
    fn_right.(seed, @value)
  end

  def <=>(other)
    @value <=> other.right_value
  end

  def to_s
    "right(#{@value})"
  end
end