Bidit.Auction = Class.create({

  //Read an element's class attribute
  //Workaround for a bug in IE where rA('class') returns null
  _readClass: function(el){
  //  return el.readAttribute('class') || el.className;
  return el.className;
  },

  _writeClass: function(el, className){
    el.className = className;
    /*typeof el.readAttribute('class') == 'string' ? el.writeAttribute('class', className) :
      el.className = className;*/
  },

  //Turns "foo_bar_baz" into "fooBarBaz"
  _camelize: function(str){
    return str.replace(/_([a-z])/g, function(match, character){
      return character.toUpperCase();
    });
  },

  //Turns "foo_bar_baz" into "FooBarBaz"
  _classify: function(str){
    var camelized = this._camelize(str);
    return camelized.substr(0,1).toUpperCase() + camelized.substr(1, camelized.length);
  },


  initialize: function(el){
    this.element = el;
    this.registerFetcher();
    this.startCounter();
    this.setupListeners();
    this.initializeStates();
    this.observe();

    //Used by getElement to cache elements
    this._elementCache = {};
  },



  bidsUrl: '/bids',
  autobidsUrl: '/autobids',



  // State machine-ish methods:
  states: [ 'upcoming', 'running', 'paused', 'closed', 'waiting', 'finished', 'init' ], //Allowed states
  defaultState: 'running',

  getState: function(){ return this.state || ''; },
  setState: function(state){
    if (state == this.getState()) return false;
    if (!this.states.include(state)) throw('setState: State "'+state+'" is not one of '+this.states)
    var oldState = this.state || '';
    this.state = state;
    //Use "state was changed" instead of "state changed" because the latter is also fired by changing the "state" attribute
    this.b.fire('state was changed', oldState, this.state);
  },

  //Sets the initial state and subscribes to state change events in order
  //to change the class name on the auction element.
  initializeStates: function(){
    var initialState = this.states.find(function(state){
      return this.element.hasClassName(state);
    }, this);

    this.setState(initialState || this.defaultState);

    //When the state changes, change the class name too
    this.b.listen('state was changed', function(from, to){
      this.element.removeClassName(from);
      this.element.addClassName(to);
    }, this);
  },





  //Attribute getter/setter methods:

  //Get a value from the element. If a method named getFooValue exists,
  //get('foo') will use that to get the value. Otherwise, a DOM element
  //is retrieved using getElement('foo'), and its contents are returned.
  get: function(name){

    //If there is a getFooValue function, return whatever that returns
    var fnName = 'get' + this._classify(name) + 'Value';
    if (this[fnName]) return this[fnName]();

    var element = this.getElement(name);
    return element && element.innerHTML;
  },

  //Set a value on the element. If a method named setFooValue exists,
  //set('foo', value) will use that. Otherwise, a DOM element is retrieved using
  //getElement('foo'), and its contents are set to +value+.
  set: function(name, value){
    var setterName = 'set'+this._classify(name)+'Value',
        oldValue = this.get(name);

    if (this[setterName]) this[setterName](value);
    else this.updateElement(name, value);

    var newValue = this.get(name);
    this.b.fire('value changed', name, oldValue, newValue);
    this.b.fire(name+' changed', oldValue, newValue);
    return newValue;
  },

  //Get the element used to get a value with this.get(). If there is a
  //method named getFooElement, it will be used for get('foo'), otherwise
  //the first element with the 'foo' class name inside the auction element
  //will be returned. If that element has a child with the class name 'value',
  //this will be returned instead.
  getElement: function(name){
    if (!this._elementCache[name]) {
      var fnName = 'get'+this._classify(name)+'Element';
      if (this[fnName]) { //Find element using custom getFooElement method
        this._elementCache[name] = this[fnName]();
      } else { //Find element using class name
        var el = this.element.down('.'+name); //First, try using just the class name
        var val = el && el.down('.value'); //Then, if the element has a child with the class "value", use that
        this._elementCache[name] = val || el; //Use val if it exists, otherwise use the .name element
      }
    }
    return this._elementCache[name];
  },

  //Returns the first child element of this.element that matches +selector+
  //Caches the element by its selector, so a subsequent query using the same
  //selector will return the previously fetched element from the cache.
  getElementBySelector: function(selector){
    if (!this._elementCache[selector]) {
      this._elementCache[selector] = this.element.down(selector);
    }

    return this._elementCache[selector];
  },

  //Used by set('foo', 'bar') to update the 'foo' element
  updateElement: function(name, value){
    var el = this.getElement(name);
    el && el.update(value);
  },

  //Override the "id" attribute getter/setter to replace the element's "id" attribute,
  //excluding any non-numerical characters.
  //
  //Example: (auction element looks like this: <div class="auction" id="auction_42">...</div>)
  //
  //auction.get('id') // => "42"
  //auction.set('id', 123); // auction.element.id == "auction_123"
  getIdValue: function(){ return this.element.readAttribute('id').replace(/\w+_/, ''); },
  setIdValue: function(id){
    this.element.writeAttribute('id',
      this.element.readAttribute('id').replace(/\d+$/, id)
    );
  },

  //Convenience methods for getting and setting state using get/set
  getStateValue: function(){ return this.getState(); },
  setStateValue: function(s){ this.setState(s); },

  getRemainingAutobidsElement: function(){ return this.element.down('.autobid .activated .remaining'); },

  getAutobidsActiveValue: function(){
    var el = this.element.down('.bidding .autobid');
    return el && el.hasClassName('active');
  },
  setAutobidsActiveValue: function(bool){
    var el = this.element.down('.bidding .autobid');
    if (el) {
      el[bool ? 'addClassName' : 'removeClassName']('active');
      el[bool ? 'removeClassName' : 'addClassName']('inactive');
    }
  },

  //Price is stored using a major and minor price element ("dollars and cents")
  getMajorCurrentPriceElement: function(){ return this.element.down('.current_price .major_price'); },
  getMinorCurrentPriceElement: function(){ return this.element.down('.current_price .minor_price'); },
  getCurrentPriceValue: function(){ return this.get('major_current_price')+'.'+this.get('minor_current_price'); },
  setCurrentPriceValue: function(price){
    price = (''+price).split('.');//Stringify and split into major, minor
    if (price[1].length == 1) price[1] = price[1]+'0';
    this.set('major_current_price', price[0]);
    this.set('minor_current_price', price[1]);
  },

  //Same with retail price
  getMajorRetailPriceElement: function(){ return this.element.down('.retail_price .major_price'); },
  getMinorRetailPriceElement: function(){ return this.element.down('.retail_price .minor_price'); },
  getRetailPriceValue: function(){ return this.get('major_retail_price')+'.'+this.get('minor_retail_price'); },
  setRetailPriceValue: function(price){
    price = (''+price).split('.');//Stringify and split into major, minor
    if (price[1].length == 1) price[1] = price[1]+'0';
    this.set('major_retail_price', price[0]);
    this.set('minor_retail_price', price[1]);
  },

  //And savings..
  getMajorSavingsElement: function(){ return this.element.down('.savings .major'); },
  getMinorSavingsElement: function(){ return this.element.down('.savings .minor'); },
  getSavingsValue: function(){ return this.get('major_savings')+'.'+this.get('minor_savings'); },
  setSavingsValue: function(price){
    price = (''+price).split('.');//Stringify and split into major, minor
    if (price[1].length == 1) price[1] = price[1]+'0';
    this.set('major_savings', price[0]);
    this.set('minor_savings', price[1]);
  },

  getLastBiddersValue: function(){
    if (!this._lastBidders) {
      var auction = this,
          el = this.element.down('.last_bidders');
      this._lastBidders = el && el.select('.bidder').map(function(li){
        return {userclass: auction._readClass(li).replace('bidder ', ''), login: li.down('.value').innerHTML}
      });
    }

    return this._lastBidders;
  },
  setLastBiddersValue: function(bidders){
    this._lastBidders = bidders;
    var auction = this,
        container = this.element.down('.last_bidders');
    if (container) {
      container.update('');
      bidders.each(function(b){;
        var li = new Element('li'),
            p = new Element('p'),
            span = new Element('span');
        auction._writeClass(li, 'bidder '+b.userclass);
        auction._writeClass(p, 'bidding_user '+b.userclass);
        auction._writeClass(span, 'value');

        span.update(b.login);
        p.insert(span);
        li.insert(p);
        container.insert(li);
      });
    }
  },

  getLeadingBidderValue: function(){
    var el = this.element.down('.leading_bidder');
    return el && {login: el.down('.value').innerHTML, userclass: this._readClass(el).replace('leading_bidder bidding_user ', '')};
  },
  setLeadingBidderValue: function(user){
    var el = this.element.down('.leading_bidder');
    el && el.replace('<p class="leading_bidder bidding_user '+user.userclass+'"><span class="value">'+(user.login || I18n.t('auctions.no_bids'))+'</span></p>');
  },

  getMostBidsValue: function(){
    var el = this.element.down('.most_bids');
    return el && {login: el.down('.value').innerHTML, userclass: this._readClass(el).replace('most_bids bidding_user ', '')};
  },
  setMostBidsValue: function(user){
    var el = this.element.down('.most_bids');
    el && el.replace('<p class="most_bids bidding_user '+user.userclass+'"><span class="value">'+user.login+'</span></p>');
  },

  getSecondsValue: function(){
    if (typeof this._seconds == 'number') return this._seconds;
    var digit1 = parseInt(this.element.down('.time_left .seconds .digit_1 .digit_value').innerHTML),
        digit2 = parseInt(this.element.down('.time_left .seconds .digit_2 .digit_value').innerHTML);

    return (digit1 * 10) + digit2;
  },
  //Seconds can only be 0 > value < 60
  setSecondsValue: function(seconds){
    if (seconds > 59) seconds = 59;
    if (seconds < 0) seconds = 0;

    this._seconds = seconds;

    var digit1 = this.getElementBySelector('.time_left .seconds .digit_1'),
        digit2 = this.getElementBySelector('.time_left .seconds .digit_2');

    this._writeClass(digit1, 'num_'+parseInt(seconds/10)+' digit_1');
    this._writeClass(digit2, 'num_'+(seconds % 10)+' digit_2');
    /*
    digit1.down('.digit_value').update(parseInt(seconds/10));
    digit2.down('.digit_value').update(seconds % 10);*/
  },

  getMinutesValue: function(){
    if (typeof this._minutes == 'number') return this._minutes;
    var digit1 = parseInt(this.element.down('.time_left .minutes .digit_1 .digit_value').innerHTML),
        digit2 = parseInt(this.element.down('.time_left .minutes .digit_2 .digit_value').innerHTML);

    return (digit1 * 10) + digit2;
  },
  setMinutesValue: function(minutes){
    if (minutes < 0) minutes = 0;

    this._minutes = minutes;

    var digit1Container = this.getElementBySelector('.time_left .minutes .digit_1'),
        digit2Container = this.getElementBySelector('.time_left .minutes .digit_2');
    //var digit1Value = digit1Container.down('.digit_value'),
    //    digit2Value = digit2Container.down('.digit_value'),
    var digit1 = parseInt(minutes/10),
        digit2 = minutes % 10;

    //if (digit1Value.innerHTML != digit1) {
      this._writeClass(digit1Container, 'num_'+digit1+' digit_1');
      //digit1Value.update(digit1);
    //}
    //if (digit2Value.innerHTML != digit2) {
      this._writeClass(digit2Container, 'num_'+digit2+' digit_2');
      //digit2Value.update(digit2);
    //}
  },

  //Time is returned as minutes converted to seconds + seconds
  getTimeValue: function(){
    return (parseInt(this.get('minutes')) * 60) + parseInt(this.get('seconds'));
  },
  //Time is set as seconds that are converted to minutes/seconds
  setTimeValue: function(seconds){
    if (seconds < 1) seconds = 1;
    this.set('minutes', Math.floor(seconds / 60));
    this.set('seconds', seconds % 60);
  },

  getTimeMessageElement: function(){
    return this.element.down('.message');
  },



  //The following states are not confined by the state machine-ish set/getState methods,
  //and can be set/unset independently of other states as normal attributes:

  //An auction will be set as updated when data inside the auction has been updated.
  getUpdatedValue: function(){ return this.element.hasClassName('updated'); },
  setUpdatedValue: function(b){ this.element[b ? 'addClassName' : 'removeClassName']('updated'); },

  //When the fetcher sends off a request, the auction will have a "loading" state
  getLoadingValue: function(){ return this.element.hasClassName('loading'); },
  setLoadingValue: function(b){ this.element[b ? 'addClassName' : 'removeClassName']('loading'); },

  //An auction will be set as "bidding" when the user presses one of the bid buttons
  getBiddingValue: function(){ return this.element.hasClassName('bidding'); },
  setBiddingValue: function(b){ this.element[b ? 'addClassName' : 'removeClassName']('bidding'); },


  //Counter state is a separate state which depends on the time left on the counter
  //Normally, "urgent" is <20 seconds and "imperative" is <10 seconds
  getCounterStateValue: function(){
    if (this.element.hasClassName('imperative')) return "imperative";
    else if (this.element.hasClassName('urgent')) return "urgent";
    else return "normal";
  },
  setCounterStateValue: function(state){
    ['urgent', 'imperative'].each(function(c){ this.element.removeClassName(c); }, this);
    if (state == 'urgent') this.element.addClassName('urgent');
    else if (state == 'imperative') this.element.addClassName('imperative');
  },

  //The "reload" attribute indicates if the page should be refreshed when the auction is finished
  getReloadValue: function(){ return this.element.hasClassName('reload'); },
  setReloadValue: function(b){ this.element[b ? 'addClassName' : 'removeClassName']('reload'); },

  //By default, the "updated" state lasts for 1 second
  updated: function(duration){
    if (this._updatedTimeout) clearTimeout(this._updatedTimeout);
    this.set('updated', true);

    var auction = this;
    this._updatedTimeout = setTimeout(function(){
      auction.set('updated', false);
    }, duration ? (duration * 1000) : 1000);
  },




  //Adds the class name "updated" for +duration+ secs to the element
  //matching +css+ which is a child of the auction element
  //
  //Example: auction.highlightValue('.price')
  highlightValue: function(css, duration){
    var element = this.element.down(css)

    if (element) {
      element.addClassName('updated');
      setTimeout(function(){
        element.removeClassName('updated');
      }, duration ? (duration * 1000) : 1000);
    }
  },

  //Called when the fetcher returns with data from the server
  updateValues: function(values){
    var auction = this,
        prev = this._previouslyReceivedValues || {};

    if (prev.current_price != values.current_price) this.set('current_price', values.current_price);
    if (prev.actual_price != values.actual_price) this.set('retail_price', values.actual_price);
    this.set('last_bidders', values.last_bidders);
    this.set('savings', values.savings);
    if (!prev.most_bids || (prev.most_bids.login != values.most_bids.login)) this.set('most_bids', values.most_bids);
    if (!prev.leading_bidder || (prev.leading_bidder.login != values.leading_bidder.login)) {
      this.set('leading_bidder', values.leading_bidder);
      //When the leading bidder gets updated, consider the auction to be updated
      //and fire off a message
      Bidit.b.fire('auction updated', this);
    }

    var time = this.get('time');
    if (!this._previouslyReceivedValues || values.seconds_left > time+5 || values.seconds_left < time-3) {
      auction.set('time', values.seconds_left);
    }

    //HACK: Don't change the state if it transitions from waiting to running and time left is < 3 secs
    //This prevents an edge case where the auction timer reaches 0, but the auction on the server is
    //actually still running with very little time left, thus momentarily setting the auction back to
    //running with 1 second left before the next update either sets the auction to finished or to
    //running with more than a couple of seconds left (someone placed a bid, resetting the timer).
    if (!(this.get('state') == 'waiting' && values.state == 'running' && values.seconds_left < 3)) {
      this.set('state', values.state);
    }

    this._previouslyReceivedValues = values;
  },

  //Add this auction ID to the fetcher. Also forwards all messages from
  //the fetcher's broadcaster to the auction's broadcaster
  registerFetcher: function(){
    var auction = this,
        fetcher = Bidit.auctionFetcher;

    fetcher.register(this.get('id'));

    //Forward all messages to the auction's broadcaster
    fetcher.b.listen('*', function(){
      auction.b.fire.apply(auction.b, arguments);
    });
  },

  unregisterFetcher: function(){
    Bidit.auctionFetcher.unregister(this.get('id'));
  },


  disableForms: function(){
    this.element.select('form').invoke('disable');
  },

  enableForms: function(){
    this.element.select('form').invoke('enable');
  },


  //Initializes a Broadcaster to make the auction element observable and able
  //to dispatch messages for interesting events. Also adds listeners for some
  //events.
  setupListeners: function(){
    var auction = this;

    this.b = this.broadcaster = new Broadcaster();
    this.b.defaultScope = this;//Listeners run with the auction as the default scope

    //When updates return from the server for this auction, update the values
    this.b.listen('status received for '+this.get('id'), function(values){
      this.updateValues(values);
    });

    //While the fetcher is getting the values from the server, the auction is
    //in the "loading" state (i.e. class "loading" gets added to the element)
    this.b.listen('loading', function(){
      this.set('loading', true);
    });
    this.b.listen('status received', function(values){
      this.set('loading', false);
    });

    this.b.listen('value changed', function(name, oldValue, newValue){
      if (name == 'time'){
        if (newValue <= 20 && newValue > 10 && oldValue > 20) auction.set('counter_state', 'urgent');
        else if (newValue <= 10 && oldValue > 10) auction.set('counter_state', 'imperative');
        else if (newValue > 20 && oldValue <= 20) auction.set('counter_state', 'normal');
        if (newValue > oldValue+1){
          auction.highlightValue('.time', 0.5);
        }
      } else if (name.match(/current_price|leading_bidder/) && (''+newValue).strip() != (''+oldValue).strip()) {
        auction.highlightValue('.'+name, 0.5);
      }
    });

    this.b.listen('state was changed', function(oldState, newState){
      if (newState == 'waiting') {
        this.set('time_message', I18n.t('auction.messages.processing'));
      } else if (newState == 'paused') {
        this.set('time_message', I18n.t('auction.messages.paused'));
      } else if (newState == 'closed') {
        this.set('time_message', 'STENGT');
      } else if (newState == 'finished') {
        if (this.get('reload')) {
          Bidit.reload();
        } else {
          Bidit.b.fire('auction finished', this);
          this.disableForms();
          this.unregisterFetcher();
          this.set('time_message', I18n.t('auction.messages.finished'));
          this.set('leading_bidder_description', I18n.t('auction.winner'));
          this.set('price_description', I18n.t('auction.sold_price'));
          this.set('savings_description', I18n.t('auction.savings.post'));
        }
      }

      if (oldState == 'finished') {
        this.enableForms();
        this.registerFetcher();
      }

      if (newState.match(/finished|paused|closed/)){
        this.stopCounter();
      } else if (oldState.match(/waiting|finished|paused|closed/) && !newState.match(/waiting|finished|paused|closed/)) {
        this.startCounter();
        this.set('leading_bidder_description', I18n.t('auction.leading_bidder.label'));
        this.set('price_description', I18n.t('auction.current_price'));
      }
    });

    Bidit.b.listen('autobid amount for auction '+auction.get('id')+' changed', function(amount, prevAmount){
      auction.set('remaining_autobids', amount);
    });

    Bidit.b.listen('autobid removed', function(autobid, amount, auctionID){
      if (auctionID == auction.get('id')) {
        auction.set('autobids_active', false);
      }
    });
  },

  observe: function(){
    var auction = this;

    this.element.select('form').each(function(form){
      form.observe('submit', function(){
        Bidit.b.fire('auction bid submitted', auction);
      });
    });
  },

  //Sets an interval which reduces the 'time' value by 1 every second
  //until it reaches 0.
  startCounter: function(){
    var auction = this;
    this._counterInterval = setInterval(function(){
      var oldTime = auction.get('time');
      var newTime = oldTime - 1;
      if (newTime <= 0) {
        auction.stopCounter();
        auction.set('time', 1);
        auction.set('state', 'waiting');
      } else {
        auction.set('time', newTime);
      }
    }, 1000);
  },

  stopCounter: function(){
    if (this._counterInterval) clearInterval(this._counterInterval);
  }

});



/* Fetcher - Fetches JSON periodically describing the state of one or more auctions
 * Auctions register themselves with their ID and receive updates through a callback function
 *
 * var fetcher = new Fetcher();
 * fetcher.register(1, function(json){ handle(json); });
 * fetcher.register(2, function(json){ handle(json); });
 */
Bidit.Auction.Fetcher = Class.create({

  //url: '/get_updates',
  url: '/auction_updates',
  initialize: function(seconds){
    this.ids = new Hash(); //Maps {id: callback} pairs, e.g. {1: fn, 2: fn}
    this.b = this.broadcaster = new Broadcaster();
    this.seconds = seconds || 1;
  },

  //Add an id with a callback. The callback function will receive the JSON
  //object after it's been retrieved from the server
  register: function(id, callback){
    this.ids.set(id, callback);
    if (callback) this.b.listen('status received for '+id, callback);
    this.b.fire('registered', id, callback);
  },

  //Remove an auction ID and its callback
  unregister: function(id){
    var fn = this.ids.get(id);
    this.b.stopListening(fn);
    this.ids.unset(id);
    this.b.fire('unregistered', id, fn);
  },

  start: function(){
    this.initializeTimeout();
    this.b.fire('started');
  },

  stop: function(){
    this.stopTimeout();
    this.b.fire('stopped');
  },

  //Called on a successful fetch with the JSON data returned from the server
  afterFetch: function(json){
    var fetcher = this,
        data = json.data;

    //Only fire if the data has been updated since the last fetch
    if (json.updated_at && (!fetcher._updated_at || fetcher._updated_at < json.updated_at)) {
      //First, broadcast all updates
      fetcher.b.fire('status received', data);

      //Then broadcast for each ID
      Object.keys(data).each(function(id){
        fetcher.b.fire('status received for '+id, data[id]);
      });

      fetcher._updated_at = json.updated_at
    }
  },

  //This method does the actual fetching, asynchronously
  fetch: function(opts){
    //Passed options (opts) override defaults
    opts = Object.extend({
      method: 'get',
      evalJSON: 'force',
      parameters: {'auction_ids[]': this.ids.keys()}
    }, opts);
    new Ajax.Request(this.url, opts);
  },

  //Creates a timeout which will re-create itself after it's been called (after the response is received),
  //effectively creating an interval that takes into account the time it takes
  //for a request to return from the server. This means long-running intervals
  //won't queue up lots of requests over time.
  initializeTimeout: function(){
    var fetcher = this;
    if (this.ids.size()) {
      this.createTimeout({
        onLoading: function(){
          fetcher.b.fire('loading');
        },
        onSuccess: function(res){
          fetcher.b.fire('success');
          fetcher.afterFetch(res.responseJSON);
        },
        onComplete: function(res){
          fetcher.b.fire('complete', res);
          fetcher.resetTimeout();
        }
      });
    } else {
      //When no auctions have registered to receive updates, try again
      //in 2 seconds
      setTimeout(function(){
        fetcher.initializeTimeout();
      }, 2000);
    }
  },

  //Creates a new timeout with +this.seconds+ which fetches the updates from the server
  createTimeout: function(opts){
    var fetcher = this;
    this._timeout = setTimeout(function(){
      fetcher.fetch(opts);
    }, this.seconds * 1000);
  },

  resetTimeout: function(){
    this.stopTimeout();
    this.initializeTimeout();
  },

  stopTimeout: function(){
    if (this._timeout) clearTimeout(this._timeout);
  }

});



Bidit.auctionFetcher = new Bidit.Auction.Fetcher(4);


//When auctions have been initialised, start the fetcher
Bidit.b.listen('auctions ready', function(){
  Bidit.auctionFetcher.start();
});

//Stop the fetcher when a new bid is submitted on an auction to
//prevent auctions from being updated before the site gets refresehd
Bidit.b.listen('auction bid submitted', function(){
  Bidit.auctionFetcher.stop();
});


Bidit.b.listen('ready', function(){
  //Initialize all auctions
  Bidit.auctions = $$('.auctions .auction:not(.upcoming)').map(function(el){
      return new Bidit.Auction(el);
  });
  Bidit.b.fire('auctions ready', Bidit.auctions);
});


Bidit.b.listen('auctions ready', function(auctions){
  if (auctions.length > 0) {
    setTimeout(function(){
      window.location = "/timeout.html";
    }, 1200000); // 1000ms * 60s * 20m = 600000
  }
});
