/**
 * NULLy JavaScript Framework, version 0.0.1
 * (c) 2010 fallroot
 *
 * Prototype.js를 확장한다.
 **/

/**
 * $E([tag]) -> Element
 * - tag (String): #id, .class 선택자를 포함한 태그 이름 (기본값: 'div')
 * - attributes (Object): 엘리먼트 속성들
 * document.createElement를 확장하여 아이디, 클래스, 속성을 한 번에 처리한다.
 **/
var $E = function(tag, attributes) {
  // 태그가 없을 경우 처리한다.
  if (!attributes && typeof tag == 'object' && tag.nodeType != 1) {
    attributes = tag;
    tag = null;
  }

  var element = null,
      id      = null,
      classes = [];

  // 이미 존재하는 엘리먼트인지 검사한다.
  if (Object.isElement(tag)) {
    element = $(tag);
  } else {
    if (tag) {
      tag = tag.strip();
      // #id 형식의 아이디를 찾는다.
      if (tag.match(/#([\w\-]+)/g)) {
        id = RegExp.$1;
      }
      // .class 형식의 클래스명을 여러 개 찾는다.
      // (IE) 정규식을 바로 while 문에 작성하면 무한루프에 빠진다.
      var re = /\.([\w\-]+)/g;
      while (re.test(tag)) {
        classes.push(RegExp.$1);
      }
      tag = tag.replace(/\s*[#\.][\w\-]+\s*/g, '');
      
      var defaults = {};
      
      if (tag == 'a') {
        defaults = {href: '#'};
      } else if (tag == 'img') {
        defaults = {alt: ''};
      } else if (tag == 'table') {
        defaults = {cellspacing: '0'};
      }
      attributes = Object.extend(defaults, attributes);
    }
    // 태그가 없으면 <div>를 기본값으로 엘리먼트를 만든다.
    element = $(document.createElement(tag || 'div'));
  }
  if (id) {
    element.setAttribute('id', id);
  }
  if (classes.length > 0) {
    element.className = classes.join(' ');
  }
  if (attributes) {
    element.writeAttribute(attributes);
  }
  return element;
};

Prototype.Browser.IE6 = Prototype.Browser.IE && !window.XMLHttpRequest;

/**
 * Number.random([base]) -> Number
 * - base (Number) : 시작하는 숫자 (기본값: 0)
 * base에서 Number까지의 숫자 중에서 무작위로 한 숫자를 반환한다.
 **/
Number.prototype.random = function(base) {
  base = base || 0;
  return Math.floor(Math.random() * (this + 1 - base)) + base;
};

/**
 * Number.commafy() -> String
 * String.commafy() -> String
 * 숫자를 3자리마다 쉼표로 구분된 문자열로 바꾼다.
 **/
(function() {
  var commafy = function() {
    var src = this.toString();
    var reg = /(^[+-]?\d+)(\d{3})/;
    while (reg.test(src)) {
      src = src.replace(reg, '$1' + ',' + '$2');
    }
    return src;
  };
  Number.prototype.commafy = commafy;
  String.prototype.commafy = commafy;
})();

/**
 * Number.toInt() -> Number
 * String.toInt() -> Number
 * parseInt 함수는 함수 연결이 되지 않아서 이 함수로 대체한다.
 **/
(function() {
  var toInt = function() {
    var value = parseInt(this);
    return isNaN(value) ? 0 : value;
  };
  Number.prototype.toInt = toInt;
  String.prototype.toInt = toInt;
})();

/**
 * Array.shuffle() -> null
 * 배열 요소를 무작위로 섞는다.
 **/
Array.prototype.shuffle = function() {
  // var length = this.length;
  // for (var i = 0; i < length; ++i) {
  //   var target = (length - 1).random();
  //   if (i != target) {
  //     var temp = this[i];
  //     this[i] = this[target];
  //     this[target] = temp;
  //   }
  // }
  this.sort(function() {
    return 0.5 - Math.random();
  });
};

/**
 * Array.remove(element) -> Object
 * - element : Object
 * 주어진 배열 요소를 삭제한다.
 **/
Array.prototype.remove = function(element) {
  return this.splice(this.indexOf(element), 1);
};

/**
 * HTML Element에 함수를 추가한다.
 **/
Element.addMethods({
  append: function(element, tag, attributes) {
    var child = $E(tag, attributes);
    element.appendChild(child);
    child._parent = element;
    return child;
  },
  appendText: function(element, text) {
    element.appendChild(document.createTextNode(text));
    return element;
  },
  appendTo: function(element, parent) {
    ($(parent) || document.body).appendChild(element);
    return element;
  },
  end: function(element) {
    return element._parent;
  },                      
	// 하위 엘리먼트들을 모두 제거한다.
	removeChildAll: function(element) {
    while (element.hasChildNodes()) {
      element.removeChild(element.lastChild);
    }
    return element;
	}
});

/*
appendElement: function(element, tag) {
  var child = Object.isElement(tag) ? $(tag) : $(document.createElement(tag || 'div'));
  element.appendChild(child);
  child._parent = element;
  return child;
},
*/

Element.addMethods('form', {
  actsAsRemote: function(form, options) {
    options = Object.extend({
      orders: [],
      validations: []
    }, options);
    
    form.observe('submit', function(event) {
      event.stop();

      var validations = options.validations;
      if (validations && validations.length > 0 && !form.validate(validations)) {
        return;
      }

      // 아직 응답 중이면 진행하지 않는다.
      if (form.hasClassName('loading')) return;
      // 폼 엘리먼트에 부여된 클래스를 초기화한다.
      form.select('.error').invoke('removeClassName', 'error');
      // 응답 메시지를 초기화한다.
      form.select('.message').invoke('remove');
      
      form.addClassName('loading');
      
      if (options.handler) {
        options.handler($H(options).unset(options.handler));
      } else {
        new Ajax.Request(options.action || form.action, {
          method: options.method || form.method,
          parameters: Object.extend({
            authenticity_token: options.authToken
          }, options.parameters || form.serialize(true)),
          onComplete: function(response) {
            form.removeClassName('loading');

            var header = response.headerJSON;
            if (header.code == 200) {
              if (options.redirect) {
                location.href = options.redirect;
              }
            } else {
              var errors = header.errors;
              if (options.orders.length > 0) {
                options.orders.each(function(order) {
                  var element = $(order);
                  var message = errors[order];
                  if (message) {
                    nully.ui.Logger.message('error', message);
                    element && element.addClassName('invalid').activate();
                    throw $break;
                  }
                });
              } else {
                $H(errors).each(function(error) {
                  var element = $(error.key) || $(error.key + '1');
                  var message = error.value;
                  nully.ui.Logger.message('error', message);
                  element && element.addClassName('invalid').activate();
                  throw $break;
                });
              }
            }
          }
        });
      }
    });
  },
  validate: function(form, inputs) {
    var valid = true;
    var messages = {
      blank: ' 입력하세요'
    };
    form.select('.invalid').invoke('removeClassName', 'invalid');
    
    inputs.each(function(input) {
      var element = $(input.id);
      if (!element) return;
      var type = input.type || 'blank';
      var message = input.message || input.name + nully.Korean.objectiveCase(input.name) + messages[type];
      if (type == 'blank') {
        if (element.value.strip().length == 0) {
          nully.ui.Logger.message('error', message);
          if (element.addClassName('invalid').visible()) {
            element.focus();
          }
          valid = false;
          throw $break;
        }
      }
    });
    return valid;
  },
  submitWithValidation: function(form, inputs) {
    form.observe('submit', function(event) {
      if (!form.validate(inputs)) {
        event.stop();
      }
    });
  }
});

/**
 * NULLy
 **/
var nully = {};

nully.Object = {
  // Prototype의 Object.extend()는 하위 Object를 처리하지 않는다.
  extend: function(target, source) {
    for (var property in source) {
      if (typeof source[property] == 'object') {
        if (!target[property]) target[property] = {};
        target[property] = nully.Object.extend(target[property], source[property]);
      } else {
        target[property] = source[property];
      }
    }
    return target;
  },
  equal: function(source, target) {
    if (typeof source != 'object') {
      return source == target;
    }
    for (var attr in source) {
      if (source[attr] !== target[attr]) {
        return false;
      }
    }
    return true;
  },
  complement: function(source, target) {
    if (typeof source != 'object') {
      return source;
    }
    // 원본에 영향을 주지 않기 위해 복사한다.
    var result = Object.extend({}, source);
    for (var attr in result) {
      if (target[attr] && result[attr] == target[attr]) {
        delete result[attr];
      }
    }
    return result;
  }
};

nully.Korean = {
  // 유니코드 2.0에서 한글은 초성 19 개, 중성 21 개, 종성 28 개로 구성되어 있다.
  // 한글은 0xAC00에서 시작하고 종성에서 0을 받침이 없음을 의미하므로 28로 나누어 떨어지는지 계산한다.
  hasFinalConsonant: function(src) {
    return (src.charCodeAt(src.length - 1) - 0xac00) % 28 != 0;
  },
  subjectiveCase: function(word) {
    return this.hasFinalConsonant(word) ? '이' : '가';
  },
  objectiveCase: function(word) {
    return this.hasFinalConsonant(word) ? '을' : '를';
  }
};

nully.Queue = Class.create({
  initialize: function() {
    this.queue = [];
    this.started = false;
  },
  add: function(fn, delay) {
    this.queue.push({
      original: fn,
      delay: delay
    });
  },
  wrap: function(original, next, delay) {
    return function() {
      original();
      delay ? setTimeout(next, delay) : next();
    }
  },
  beforeStart: function() {
    var length = this.queue.length;
    for (var i = length - 1; i > 0; --i) {
      var curr = this.queue[i];
      var prev = this.queue[i - 1];
      prev.wrap = this.wrap(prev.original, curr.wrap || curr.original, prev.delay);
    }
    if (length == 1) this.queue[0].wrap = this.queue[0].original;
    this.started = true;
  },
  start: function() {
    if (!this.started) this.beforeStart();
    this.queue[0].wrap();
  }
});

nully.Queue2 = Class.create({
  initialize: function() {
    this.queue  = [];
    this.index   = 0;
    this.elapsed = 0;
  },
  length: function() {
    return this.queue.length;
  },
  add: function(fn, interval) {
    this.queue.push({
      fn: fn,
      interval: interval
    });
  },
  start: function() {
    this.intervalFn = setInterval(this.loop.bind(this), 500);
  },
  restart: function() {
    if (this.intervalFn) {
      clearInterval(this.intervalFn);
    }
    this.index   = 0;
    this.elapsed = 0;
    this.start();
  },
  loop: function() {
    this.elapsed += 500;
    
    var sum = this.queue.slice(0, this.index).inject(0, function(acc, n) {
      return acc + n.interval;
    });
    
    // console.debug(this.elapsed + ':' + sum);
    
    if (this.elapsed > sum) {
      if (this.index == this.queue.length - 1) {
        clearInterval(this.intervalFn);
        this.intervalFn = null;
      }
      this.queue[this.index++].fn();
    }
  },
  changeInterval: function(interval) {
    this.queue.each(function(item) {
      item.interval += interval;
    });
  }
});

/* <head> 관련 함수 */
nully.Header = {
  // 스타일시트 또는 자바스크립트 파일을 불러온다.
  load: function(url, options) {
    options = Object.extend({
      reload: true
    }, options);
    
    if (!options.kind) {
      if (url.match(/[^\/\\]+\.(css|js)/gi)) {
        options.kind = RegExp.$1;
      } else {
        throw 'file extension should be .css or .js';
      }
    }
    
    var attr   = null,
        params = null;
    
    if (options.kind == 'css') {
      attr = 'href';
      params = {
        element: 'link',
        attrs: {
          rel : 'stylesheet',
          type: 'text/css',
          href: url
        }
      };
    } else if (options.kind == 'js') {
      attr = 'src';
      params = {
        element: 'script',
        attrs: {
          type: 'text/javascript',
          src : url
        }
      };
    } else {
      throw 'file should be stylesheet or javascript';
    }
    
    var elements = $$(params.element + '[' + attr + '="' + params.attrs[attr] + '"]');
    if (elements.length > 0) {
      if (options.reload) {
        elements.invoke('remove');
      } else {
        return;
      }
    }
    
    var element = $E(params.element, params.attrs);
    
    if (options.callback) {
      element.onreadystatechange = function () {
        if (element.readyState == 'loaded' || element.readyState == 'complete') {
          options.callback();
        }
      }                            
      element.observe('load', options.callback);
    }
      
    element.appendTo($$('head')[0]);
  }
};

nully.Cookie = {
  set: function(key, value, days) {
    var cookie = key + '=' + escape(value);
    
    if (days) {
      var date = new Date();
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
      cookie += '; expires=' + date.toGMTString();
    }
    
    document.cookie = cookie + '; path=/';
  },
  get: function(key) {
    var value = null;
    document.cookie.split(';').each(function(cookie){
      var splits = cookie.strip().split('=');
      if (splits[0] == key) {
        value = unescape(splits[1]);
      }
    });
    return value;
  },
  clear: function(key) {
    this.set(key, '', -1);
  },
  clearAll: function() {
    document.cookie.split(';').each(function(cookie){
      this.clear(cookie.strip().split('=')[0]);
    }.bind(this));
  }
};

nully.ui = {};

