/*
 * spa.model.js
 * Moduł Modelu.
*/

/*jslint         browser : true, continue : true,
  devel  : true, indent  : 2,    maxerr   : 50,
  newcap : true, nomen   : true, plusplus : true,
  regexp : true, sloppy  : true, vars     : false,
  white  : true
*/
/*global TAFFY, $, spa */

spa.model = (function () {
  'use strict';
  var
    configMap = { anon_id : 'a0' },
    stateMap  = {
      anon_user      : null,
      cid_serial     : 0,
      is_connected   : false,
      people_cid_map : {},
      people_db      : TAFFY(),
      user           : null
    },

    isFakeData = true,

    personProto, makeCid, clearPeopleDb, completeLogin,
    makePerson, removePerson, people, chat, initModule;

// Interfejs API obiektu people.
// ---------------------
// Obiekt people jest dostępny w spa.model.people.
// Obiekt people zapewnia metody i zdarzenia służące do zarządzania kolekcją obiektów person. Jego metody publiczne obejmują:
//   * get_user () — zwraca obiekt person bieżącego użytkownika. Jeśli bieżący użytkownik nie jest zalogowany, zwracany jest anonimowy obiekt person.
//   * get_db () — zwraca wstępnie posortowaną bazę danych TaffyDB wszystkich obiektów person, w tym bieżącego użytkownika.
//   * get_by_cid( <id_klienta> ) — zwraca obiekt person z dostarczonym unikatowym identyfikatorem.
//   * login( <nazwa_użytkownika> ) — logowanie jako użytkownik z dostarczoną nazwą użytkownika. Obiekt bieżącego użytkownika zostanie zmieniony w celu odzwierciedlenia nowej tożsamości.
//   * logout() — przywraca obiekt bieżącego użytkownika do obiektu użytkownika anonimowego.
//
// Globalne zdarzenia niestandardowe jQuery publikowane przez ten obiekt obejmują:
//   * 'spa-login' — publikowane, gdy zakończony zostaje proces logowania użytkownika. Zaktualizowany obiekt użytkownika jest dostarczany jako dane.
//   * 'spa-logout' — publikowane po zakończeniu procesu wylogowania. Obiekt poprzedniego użytkownika jest dostarczany jako dane.
//
// Każda osoba jest reprezentowana przez obiekt person. Obiekty person dostarczają następujące metody:
//   * get_is_user() — zwraca true, jeśli obiektem jest bieżący użytkownik.
//   * get_is_anon() — zwraca true, jeśli obiektem jest anonimowy użytkownik.
//
// Do atrybutów obiektu person należą:
//   * cid — łańcuch znaków dla identyfikatora klienta. Jest zawsze zdefiniowany i różni się od atrybutu id tylko wtedy, kiedy dane klienta nie są zsynchronizowane z back-endem.
//   * id — unikatowy identyfikator. Może być niezdefiniowany, jeśli obiekt nie jest zsynchronizowany z back-endem.
//   * name — łańcuch znaków dla nazwy użytkownika.
//   * css_map — mapa atrybutów wykorzystywana do prezentacji awatara.
//

  personProto = {
    get_is_user : function () {
      return this.cid === stateMap.user.cid;
    },
    get_is_anon : function () {
      return this.cid === stateMap.anon_user.cid;
    }
  };

  makeCid = function () {
    return 'c' + String( stateMap.cid_serial++ );
  };

  clearPeopleDb = function () {
    var user = stateMap.user;
    stateMap.people_db      = TAFFY();
    stateMap.people_cid_map = {};
    if ( user ) {
      stateMap.people_db.insert( user );
      stateMap.people_cid_map[ user.cid ] = user;
    }
  };

  completeLogin = function ( user_list ) {
    var user_map = user_list[ 0 ];
    delete stateMap.people_cid_map[ user_map.cid ];
    stateMap.user.cid     = user_map._id;
    stateMap.user.id      = user_map._id;
    stateMap.user.css_map = user_map.css_map;
    stateMap.people_cid_map[ user_map._id ] = stateMap.user;

    // Kiedy dodajemy czat, powinniśmy dołączyć go tutaj.
    $.gevent.publish( 'spa-login', [ stateMap.user ] );
  };

  makePerson = function ( person_map ) {
    var person,
      cid     = person_map.cid,
      css_map = person_map.css_map,
      id      = person_map.id,
      name    = person_map.name;

    if ( cid === undefined || ! name ) {
      throw 'id i nazwa klienta są wymagane';
    }

    person         = Object.create( personProto );
    person.cid     = cid;
    person.name    = name;
    person.css_map = css_map;

    if ( id ) { person.id = id; }

    stateMap.people_cid_map[ cid ] = person;

    stateMap.people_db.insert( person );
    return person;
  };

  removePerson = function ( person ) {
    if ( ! person ) { return false; }
    // Nie można usunąć osoby anonimowej.
    if ( person.id === configMap.anon_id ) {
      return false;
    }

    stateMap.people_db({ cid : person.cid }).remove();
    if ( person.cid ) {
      delete stateMap.people_cid_map[ person.cid ];
    }
    return true;
  };

  people = (function () {
    var get_by_cid, get_db, get_user, login, logout;

    get_by_cid = function ( cid ) {
      return stateMap.people_cid_map[ cid ];
    };

    get_db = function () { return stateMap.people_db; };

    get_user = function () { return stateMap.user; };

    login = function ( name ) {
      var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();

      stateMap.user = makePerson({
        cid     : makeCid(),
        css_map : {top : 25, left : 25, 'background-color':'#8f8'},
        name    : name
      });

      sio.on( 'userupdate', completeLogin );

      sio.emit( 'adduser', {
        cid     : stateMap.user.cid,
        css_map : stateMap.user.css_map,
        name    : stateMap.user.name
      });
    };

    logout = function () {
      var is_removed, user = stateMap.user;
      // when we add chat, we should leave the chatroom here

      is_removed    = removePerson( user );
      stateMap.user = stateMap.anon_user;

      $.gevent.publish( 'spa-logout', [ user ] );
      return is_removed;
    };

    return {
      get_by_cid : get_by_cid,
      get_db     : get_db,
      get_user   : get_user,
      login      : login,
      logout     : logout
    };
  }());

// Interfejs API obiektu chat.
// -------------------
// Obiekt chat jest dostępny w spa.model.chat.
// Obiekt czat dostarcza metody i zdarzenia do zarządzania
// wiadomościami czatu. Do jego metod publicznych należą:
//   * join() — dołączanie do pokoju czatu. Ta procedura konfiguruje
//     protokół czatu z back-endem, włączając w to publikatory dla
//     globalnych zdarzeń niestandardowych 'spa-listchange' i 
//     'spa-updatechat'. Jeśli bieżący użytkownik jest anonimowy,
//     metoda join() przerywa wykonywanie i zwraca false.
// ...
//
// Do globalnych zdarzeń niestandardowych jQuery publikowanych przez ten obiekt należą:
// ...
//   * spa-listchange — zdarzenie publikowane, gdy zmienia się długość
//     listy osób online (np. kiedy osoba wchodzi na czat
//     lub z niego wychodzi) albo gdy zmienia się jej zawartość
//     (np. gdy jakaś osoba zmienia szczegóły awatara).
//     Subskrybent tego zdarzenia powinien otrzymać od modelu użytkowników 
//     kolekcję people_db dla aktualnych danych.
// ...
//
  chat = (function () {
    var
      _publish_listchange,
      _update_list, _leave_chat, join_chat;


    // Rozpoczęcie metod wewnętrznych.
    _update_list = function( arg_list ) {
      var i, person_map, make_person_map,
        people_list      = arg_list[ 0 ];

      clearPeopleDb();

      PERSON:
      for ( i = 0; i < people_list.length; i++ ) {
        person_map = people_list[ i ];

        if ( ! person_map.name ) { continue PERSON; }

        // Jeśli użytkownik jest zdefiniowany, zaktualizuj css_map i pomiń resztę.        
		if ( stateMap.user && stateMap.user.id === person_map._id ) {
          stateMap.user.css_map = person_map.css_map;
          continue PERSON;
        }

        make_person_map = {
          cid     : person_map._id,
          css_map : person_map.css_map,
          id      : person_map._id,
          name    : person_map.name
        };

        makePerson( make_person_map );
      }

      stateMap.people_db.sort( 'name' );
    };

    _publish_listchange = function ( arg_list ) {
      _update_list( arg_list );
      $.gevent.publish( 'spa-listchange', [ arg_list ] );
    };
    // Zakończenie metod wewnętrznych.

    _leave_chat = function () {
      var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
      stateMap.is_connected = false;
      if ( sio ) { sio.emit( 'leavechat' ); }
    };

    join_chat  = function () {
      var sio;

      if ( stateMap.is_connected ) { return false; }

      if ( stateMap.user.get_is_anon() ) {
        console.warn( 'Przed wejściem do pokoju czatu należy zdefiniować użytkownika');
        return false;
      }

      sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
      sio.on( 'listchange', _publish_listchange );
      stateMap.is_connected = true;
      return true;
    };

    return {
      _leave : _leave_chat,
      join   : join_chat
    };
  }());

  initModule = function () {
    // Inicjowanie osoby anonimowej.
    stateMap.anon_user = makePerson({
      cid   : configMap.anon_id,
      id    : configMap.anon_id,
      name : 'anonimowy'
    });
    stateMap.user = stateMap.anon_user;
  };

  return {
    initModule : initModule,
    chat       : chat,
    people     : people
  };
}());
