// calcform.js
/*
概要:
	HTML の form を使用してカスタム計算機を作成するためのライブラリ

使用例: 
	bpmcalc.html ( http://drumsoft.com/100yen/bpmcalc.html ) を参照

使い方: 
	計算ルールオブジェクト myrules を作成して
	CalcForm.init(myrules);
	とする。以上。

計算ルールオブジェクト:
	"フォーム名.フィールド名" という文字列でドキュメント中の input 要素を
	特定する。この文字列を フィールド指定 、 input 要素を フィールド と呼ぶ。

	各フィールドについて、値を計算する為のプロパティを以下の様に記述する
	"フィールド指定":{exp:function(form){..}, depends:["フィールド指定1",..]}
	
	exp は引数を一つとる無名関数で、フィールドの取るべき値を計算して返す。
	CalcForm オブジェクトを引数としてコールされるので、以下のメソッド
	.value("フィールド指定") : フィールドの値を取得
	.last("フィールド指定1","フィールド指定2",..) : 最後に更新されたフィールド表記を返す
	を使用できる。
	
	depends は配列で、このフィールドが依存するフィールドのフィールド表記を
	持たせる。

	isValid という名前のメソッドを定義しなければならない
	isValid は引数を一つ取り、その値が計算結果として有効かどうかの真偽値を返す

	それ以外の "." を含まないプロパティは無視されるので、
	プライベートメソッドやメンバ変数を定義してよい
	exp 関数は計算ルールオブジェクトそのもののメンバメソッドとして
	コールされるので、これらのプライベートメソッドやメンバ変数を
	this を通して利用する事ができる。
*/

/*
"calcform.js"
Copyright (C) 2008 Haruka Kataoka

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/


var CalcForm = {
	//他のオブジェクトに apply するメソッド

	//Element.addEventListener .. 
	addEventListener: function(event, fun) {
		if(this.attachEvent) {
			this.attachEvent("on"+event, function(){return fun(window.event)});
		}else{
			this.addEventListener(event, fun, false);
		}
	},
	//Element.dispachCalc .. 計算を実行するイベントハンドラ
	dispachCalc: function(ev) {
		var src = ev.srcElement ? ev.srcElement : ev.target;
		var form = src.parentNode;
		while(!form.tagName.match(/form/i)){
			form = form.parentNode;
			if (!form) break;
		}
		var field = form.name + "." + src.name;
		CalcForm.startCalc(field);
	},
	//Array.exPush .. exclusive Push
	exPush: function(elm){
		var l = this.length;
		for(var i = 0; i < l; i++) {
			if (this[i] == elm) return elm;
		}
		return this.push(elm);
	},
	//Array.exUnshift .. exclusive Unshift
	exUnshift: function(elm){
		var l = this.length;
		for(var i = 0; i < l; i++) {
			if (this[i] == elm) {
				this.splice(i, 1);
				break;
			}
		}
		return this.unshift(elm);
	},

	//メンバ変数

	//更新ルール (init で設定される)
	rules: null,
	//逆依存グラフ(各フィールド更新後に更新が必要なフィールド)
	//被依存フィールド名:{依存元フィールド名:1, ..}, ..
	pullups: {},
	//手で値が入力されたフィールドの更新順序を保持する（フィールド名の配列）
	recents: [],
	//フォームの値の保存を行うハッシュ
	preserve: {},
	//1つの再計算セッションで更新されるフィールド名のキュー
	calcQue: [],
	//1つの再計算セッションで既に更新済のフィールド名をマークする
	//（更新済みフィールド名をキーに持つハッシュ）
	calced: {},

	//メソッド
	
	//初期化1
	init: function(arules){
		var me = this;
		this.rules = arules;
		this.addEventListener.apply(window, ["load", function(){me.setup()}]);
	},
	//初期化2
	setup: function(){
		//各フィールドにイベントを登録
		var inputs = document.getElementsByTagName("input");
		for( var i = 0; i < inputs.length; i++ ){
			this.addEventListener.apply(inputs[i], ["change", this.dispachCalc]);
			this.addEventListener.apply(inputs[i], ["keyup", this.dispachCalc]);
		}
		//rules の depends リストから pullups リストを作成する
		for(var idx in this.rules) {
			if ( ! idx.match(/\./) ) continue;
			if ("depends" in this.rules[idx]) {
				for(var i = 0; i < this.rules[idx].depends.length; i++) {
					var dependon = this.rules[idx].depends[i];
					if (!(dependon in this.pullups)) {
						this.pullups[dependon] = {};
					}
					this.pullups[dependon][idx] = 1;
				}
			}
			this.preserve[idx] = this.value(idx);
			if (this.preserve[idx] != "") {
				this.recents.unshift(idx);
			}else{
				this.recents.push(idx);
				this.exPush.apply(this.calcQue, [idx]);
			}
		}
		this.consumeCalcQue();
	},
	//field を更新して、依存グラフの計算を開始する
	startCalc: function(field) {
		var updated = this.value(field);
		if (!this.rules.isValid(updated) || 
			String(this.preserve[field]) == String(updated) ) 
			return;
		this.preserve[field] = updated;
		//recents を更新する
		this.exUnshift.apply(this.recents, [field]);
		//calced , calcQue を初期化
		this.calced = {};
		this.calced[field] = 1;
		this.calcQue = [];
		this.enqueueDepends(field);
		this.consumeCalcQue();
	},
	//calcQueを全て再計算する
	consumeCalcQue: function() {
		var process = true;
		while(process && this.calcQue.length > 0) {
			var idx = this.calcQue.shift();
			this.calced[idx] = 1;
			process = this.recalc(idx);
			this.enqueueDepends(idx);
		}
	},
	//field の値を再計算してセットする
	//計算結果が有効な数であった場合だけ true を返す
	recalc: function(field) {
		if ("exp" in this.rules[field]) {
			var names = field.split(".", 2);
			var result = this.rules[field].exp.apply(this.rules, [this]);
			document.forms[names[0]].elements[names[1]].value = result;
			if (String(this.preserve[field]) != String(result)) {
				this.enlight(field);
				this.preserve[field] = result;
			}
			if ( this.rules.isValid(result) ) {
				return true;
			}
		}
		return false;
	},
	//更新フィールド field に依存するフィールドを calcQue に追加する
	//最近更新したフィールド程後回しにする
	enqueueDepends: function(field) {
		if ( field in this.pullups ) {
			for (var i = this.recents.length - 1; i > 0; i-- ) {
				var depend = this.recents[i];
				if ((depend in this.pullups[field]) && !(depend in this.calced)) {
					this.exPush.apply(this.calcQue, [depend]);
				}
			}
		}
	},
	
	//更新されたフィールドを光らせるための変数とメソッド
	//色を変えたい場合、 CalcForm.litColor と CalcForm.defColor を上書きすること
	litColor: [255,160,160], 
	defColor: [255,255,255],
	duration: 300,
	timer: null,
	lighted: {},
	//光らせを開始する
	enlight: function(field) {
		var me = this;
		this.lighted[field] = new Date();
		if ( this.timer == null ) {
			this.timer = setInterval(function(){me.elapse()} , 1);
		}
	},
	//時間経過でタイマーからコールバックされるメソッド
	elapse: function() {
		var now = new Date();
		for( var idx in this.lighted ) {
			var elapsed = now - this.lighted[idx];
			if ( elapsed >= this.duration ) {
				this.element(idx).style.backgroundColor = 
					this.mixColor(this.defColor, this.litColor, 1);
				delete this.lighted[idx];
			}else{
				this.element(idx).style.backgroundColor = 
					this.mixColor(this.defColor, this.litColor, elapsed / this.duration);
			}
		}
		var idx = null;
		for( idx in this.lighted ) break;
		if ( idx == null ) {
			clearInterval( this.timer );
			this.timer = null;
		}
	},
	//色 A, B を混ぜて、 css の "rgb(r,g,b)" カラー指定文字列を返す
	mixColor: function(colorA, colorB, mix) {
		var mixed = new Array(3);
		for ( var i = 0; i < 3 ; i++ ){
			mixed[i] = Math.floor(colorA[i] * mix + colorB[i] * (1 - mix));
		}
		return "rgb(" + mixed[0] + "," + mixed[1] + "," + mixed[2] + ")";
	},
	
	//rules の exp メソッドから利用するためのメソッド
	
	//フィールド指定からその値を返す
	value: function(field) {
		var names = field.split(".", 2);
		return document.forms[names[0]].elements[names[1]].value;
	},
	//フィールド指定のリストを引数に取り、最後に更新されたフィールド指定を返す
	last: function() {
		var fields = {}, l;
		l = arguments.length;
		for(i = 0; i < l; i++) {
			fields[arguments[i]] = 1;
		}
		l = this.recents.length;
		for(i = 0; i < l; i++) {
			if(this.recents[i] in fields) return this.recents[i];
		}
		return arguments[0];
	},
	//フィールド指定からそのオブジェクトを返す
	element: function(field) {
		var names = field.split(".", 2);
		return document.forms[names[0]].elements[names[1]];
	}
}



