
# Copyright © 2008 Joshua Choi.
# This application and its code is licensed under the GNU General Public License
# from the Free Software Foundation.
#
# The GNU General Public License is a Free Software license. Like any Free
# Software license, it grants to you the four following freedoms:
#
# * The freedom to run the program for any purpose.
# * The freedom to study how the program works and adapt it to your needs.
# * The freedom to redistribute copies so you can help your neighbor.
# * The freedom to improve the program and release your improvements to the
#   public, so that the whole community benefits.
# * You may exercise the freedoms specified here provided that you comply with
#   the express conditions of this license. The principal conditions are:
# 
# You must conspicuously and appropriately publish on each copy distributed an
# appropriate copyright notice and disclaimer of warranty and keep intact all
# the notices that refer to this License and to the absence of any warranty; and
# give any other recipients of the Program a copy of the GNU General Public
# License along with the Program. Any translation of the GNU General Public
# License must be accompanied by the GNU General Public License.
#
# If you modify your copy or copies of the program or any portion of it, or
# develop a program based upon it, you may distribute the resulting work
# provided you do so under the GNU General Public License. Any translation of
# the GNU General Public License must be accompanied by the GNU General Public
# License.
#
# If you copy or distribute the program, you must accompany it with the complete
# corresponding machine-readable source code or with a written offer, valid for
# at least three years, to furnish the complete corresponding machine-readable
# source code.
#
# Any of the above conditions can be waived if you get permission from the
# copyright holder.

require 'forwardable'

APP_NAME = 'Equali'
PLUS_SIGN = '+'
MINUS_SIGN = '-'
TIMES_SIGN = '×'
DIVIDE_SIGN = '÷'
RAW_TIMES_SIGN = '*'
RAW_DIVIDE_SIGN = '/'
OPEN_PARENTHESIS = '('
CLOSED_PARENTHESIS = ')'
EQUALS_SIGN = '='
DECIMAL_POINT = '.'

class ::Fixnum

   def / num
      self.to_f / num.to_f
   end
	 
end

class AutoTruncatingArray
	extend Forwardable
	attr_accessor :limit
	
	def initialize limit
		@limit = limit
		@contents = []
	end
	
	def_delegators :@contents, :[], :each, :each_index, :size, :length, :shift,
		:pop
	
	def []= index, object
		if index > @limit
			raise IndexError, "attempted to assign to index #{index} above limit"
		end
		@contents[index] = object
	end
	
	def push *objects
		objects.each do |object|
			if size >= @limit
				shift
			end
			@contents.push object
		end
	end
	
end

class CyclicIterator
	attr_reader :first, :last
	
	def initialize last, first=0, increment=1
		@last = last
		@first = first
		@increment = increment
		@current = @first
		@comparison = :>
		if last > first and increment < 0 or last < first and increment > 0
			raise ArgumentError, 'arguments form an infinite loop'
		end
	end
	
	def succ
		old_value = @current
		@current += @increment
		if @current.send @comparison, @last
			@current = @first
		end
		return old_value
	end
	
	def reset
		@current = @first
	end
	
end

class Calculator
	ILLEGAL_RULE = / [A-Za-z] /x
	
  def eval expression
		expression = expression.to_s
		expression.sub! TIMES_SIGN, '*'
		expression.sub! DIVIDE_SIGN, '/'
		if ILLEGAL_RULE.match expression
			return nil
		end
		begin
			Kernel::eval(expression)
		rescue RuntimeError, SyntaxError
			nil
		end
	end
	
end

class Action
	attr_accessor :sign, :procedure
	
	def initialize sign, &procedure
		@sign = sign
		@procedure = procedure
	end
	
	def to_proc
		@procedure
	end
	
	def call
		@procedure.call
	end
	
	def to_s
		@sign
	end
	
end

Shoes.app :title => APP_NAME, :width => 250, :height => 320,
		:resizable => false do

	# Instance variables:
	# @answered: Has the user clicked the '=' button and got an answer?
	# @display: The EditBox that displays the currently inputted
	#   expression and answers too.
	# @status_bar: A text box that contains directions for the user.
	# @undo: The string of the inputted expression last cleared.
	# @undo_history: A self-truncating array of the strings of the inputted
	#   expressions saved to maybe be undoed to. Its size limit is determined by
	#   @undo_limit.
	# @undo_counter: A cyclic iterator that tells when it's time to save an input
	#		into the undo history.
	# @undo_limit: Determines the limit of the size of the undo history.
	# @undo_rate: How many keystrokes per undo to record an undo.
	
	# The following Procs are for action buttons.
	
	CLEAR = proc {
		@answered = false
		record_undo true
		@display.text = nil
	}
	SHOW_ANSWER = proc {
		if blank_display?
			nil
		elsif @output.nil?
			alert 'I can\'t figure out the expression. Did you leave an ' +
				'unclosed parenthesis? Or maybe there\'s an unallowed ' +
				'symbol. In any case, edit the expression until it\'s ' +
				'valid again.'
		else
			@display.text = @output
			@answered = true
			display_validation
		end
	}
	UNDO = proc {
		@undo_counter.reset
		@display.text = @undo_history.pop
	}
	DELETE = proc {
		@display.text = @display.text.chop
	}
	
	# The following strings are possible information that @status_bar may
	# display.
	
	DEFAULT_STATUS = 'Use the buttons or keyboard to type in ' +
		'numbers.'
	VALID_STATUS = 'Continue entering the expression. ',
		'When you\'re done, click on the equals sign to display the value.'
	ERROR_STATUS = 'The current expression is invalid. ',
		'Close parentheses and delete invalid characters.'
	ANSWERED_STATUS = 'The expression has been solved, and its result ' +
		'is above. You can copy the result to use in another ' +
		'application, or use the result in a new expression right now.'
	
	# The following Colors are for use through the app for a color scheme.
	
	BACKGROUND_COLOR = black
	OK_COLOR = rgb 100, 255, 100
	NOT_OK_COLOR = rgb 255, 255, 100
	
	# The following methods are for performing common actions in the app.
	
	def face_button sign, options={}
		if sign.kind_of? Action
			action_button sign, options
		else
			input_button sign, options
		end
	end
	
  def input_button input, options={}
    normal_button input, options do
			@answered = false
      @display.text += input.to_s
    end
  end
	
	def action_button action, options={}
		normal_button action, options, &action
	end
	
	def normal_button label, options={}, &action
		options = {:width => 60}.merge options
		button label, options do
			do_action &action
		end
	end
	
  def button_row *signs
    flow do
      signs.each do |sign|
				face_button sign
      end
    end
  end
	
	def action sign, &procedure
		Action.new sign, &procedure
	end
	
	def evaluate
		@output = @calculator.eval @display.text
		display_validation
	end
	
	def display_validation
		@status_bar.text = if blank_display?
				DEFAULT_STATUS
			elsif @output.nil?
				ERROR_STATUS
			elsif @answered
				ANSWERED_STATUS
			else
				VALID_STATUS
			end
	end
	
	def blank_display?
		text = display_text
		text.nil? or text.empty?
	end
	
	def display_text
		@display.text
	end
	
	def record_undo definitely_now=false
		if definitely_now or @undo_counter.succ == @undo_rate - 2
			@undo_history.push display_text
		end
	end
	
	def do_action &action
		record_undo
		action.call
		evaluate
	end
	
	# Here is the actual application.
	
	@undo_rate = 5
	@undo_limit = 10
  @calculator = Calculator.new
	@undo_history = AutoTruncatingArray.new @undo_limit
	@undo_counter = CyclicIterator.new @undo_rate - 1
	background BACKGROUND_COLOR
  stack :margin => 5 do
		@display = edit_line :margin => 5, :width => 0.95 do |line|
			evaluate
 		end
		flow do
			action_button action('Clear', &CLEAR), :width => nil
			action_button action('Undo', &UNDO), :width => nil
		end
		button_row action('⌫', &DELETE), OPEN_PARENTHESIS,
			CLOSED_PARENTHESIS, DIVIDE_SIGN
		button_row 7, 8, 9, TIMES_SIGN
		button_row 4, 5, 6, MINUS_SIGN
		button_row 1, 2, 3, PLUS_SIGN
		button_row 0, DECIMAL_POINT,
			action("   #{EQUALS_SIGN}  ", &SHOW_ANSWER)
  end
	flow do
		# @status_background = background GREEN, :radius => 3
		@status_bar = inscription :size => 8, :stroke => white
	end
	evaluate

end

