/*
 * 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,
    setDataMode;

// 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;
    chat.join();
    $.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 user = stateMap.user;

      chat._leave();
      stateMap.user = stateMap.anon_user;
      clearPeopleDb();

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

    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.
//   * get_chatee() — zwraca obiekt person osoby, z którą użytkownik
//     rozmawia na czacie. Jeśli nie ma żadnego rozmówcy, zwracana jest wartość null.
//   * set_chatee( <person_id> ) — ustawia jako rozmówcę osobę
//     identyfikowaną przez person_id. Jeśli dane person_id nie istnieje 
//     na liście osób, dla rozmówcy jest ustawiane null. Jeśli 
//     żądana osoba jest już rozmówcą, zwracana jest wartość false.
//     Ta metoda publikuje globalne zdarzenie niestandardowe 'spa-setchatee'.
//   * send_msg (<msg_text>) — wysyła wiadomość do rozmówcy.
//     Publikuje globalne zdarzenie niestandardowe 'spa-updatechat'.
//     Jeśli użytkownik jest anonimowy lub rozmówcą jest null,
//     metoda przerywa wykonywanie i zwraca false.
//   * update_avatar (<update_avtr_map>) — wysyła update_avtr_map
//     do back-endu. Wywołuje to zdarzenie 
//     'spa-listchange', które publikuje zaktualizowaną
//     listę osób i informacje awatara (css_map w obiektach person).
//     update_avtr_map musi mieć formę
//     { person_id: person_id, css_map: css_map }.
//
// Do globalnych zdarzeń niestandardowych jQuery publikowanych przez ten obiekt należą:
//   * spa-setchatee — zdarzenie publikowane, gdy ustawiany jest nowy rozmówca.
//     Jako dane dostarczana jest mapa w postaci:
//       { old_chatee : <old_chatee_person_object>,
//         new_chatee : <new_chatee_person_object>
//       }.
//   * 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.
//   * spa-updatechat — zdarzenie publikowane, gdy nowa wiadomość jest
//     odbierana lub wysyłana. Jako dane dostarczana jest mapa w postaci:
//       { dest_id   : <chatee_id>,
//         dest_name : <chatee_name>,
//         sender_id : <sender_id>,
//         msg_text  : <message_content>
//       }.
//
  chat = (function () {
    var
      _publish_listchange, _publish_updatechat,
      _update_list, _leave_chat,

      get_chatee, join_chat, send_msg,
      set_chatee, update_avatar,

      chatee = null;

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

      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
        };
        person = makePerson( make_person_map );

        if ( chatee && chatee.id === make_person_map.id ) {
          is_chatee_online = true;
          chatee = person;
        }
      }

      stateMap.people_db.sort( 'name' );

      // Jeśli rozmówca nie jest już online, wyłączamy go,
      // co wywołuje globalne zdarzenie 'spa-setchatee'.
      if ( chatee && ! is_chatee_online ) { set_chatee(''); }
    };

    _publish_listchange = function ( arg_list ) {
      _update_list( arg_list );
      $.gevent.publish( 'spa-listchange', [ arg_list ] );
    };

    _publish_updatechat = function ( arg_list ) {
      var msg_map = arg_list[ 0 ];

      if ( ! chatee ) { set_chatee( msg_map.sender_id ); }
      else if ( msg_map.sender_id !== stateMap.user.id
        && msg_map.sender_id !== chatee.id
      ) { set_chatee( msg_map.sender_id ); }

      $.gevent.publish( 'spa-updatechat', [ msg_map ] );
    };
    // Zakończenie metod wewnętrznych.

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

    get_chatee = function () { return chatee; };

    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 );
      sio.on( 'updatechat', _publish_updatechat );
      stateMap.is_connected = true;
      return true;
    };

    send_msg = function ( msg_text ) {
      var msg_map,
        sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();

      if ( ! sio ) { return false; }
      if ( ! ( stateMap.user && chatee ) ) { return false; }

      msg_map = {
        dest_id   : chatee.id,
        dest_name : chatee.name,
        sender_id : stateMap.user.id,
        msg_text  : msg_text
      };

      // Opublikowaliśmy zdarzenie updatechat, więc możemy pokazać nasze wychodzące wiadomości.
      _publish_updatechat( [ msg_map ] );
      sio.emit( 'updatechat', msg_map );
      return true;
    };

    set_chatee = function ( person_id ) {
      var new_chatee;
      new_chatee  = stateMap.people_cid_map[ person_id ];
      if ( new_chatee ) {
        if ( chatee && chatee.id === new_chatee.id ) {
          return false;
        }
      }
      else {
        new_chatee = null;
      }

      $.gevent.publish( 'spa-setchatee',
        { old_chatee : chatee, new_chatee : new_chatee }
      );
      chatee = new_chatee;
      return true;
    };

    // Mapa avatar_update_map powinna mieć postać:
    // { person_id : <string>, css_map : {
    //   top : <int>, left : <int>,
    //   'background-color' : <string>
    // }};
    //
    update_avatar = function ( avatar_update_map ) {
      var sio = isFakeData ? spa.fake.mockSio : spa.data.getSio();
      if ( sio ) {
        sio.emit( 'updateavatar', avatar_update_map );
      }
    };

    return {
      _leave        : _leave_chat,
      get_chatee    : get_chatee,
      join          : join_chat,
      send_msg      : send_msg,
      set_chatee    : set_chatee,
      update_avatar : update_avatar
    };
  }());

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

  setDataMode = function ( arg_str ) {
    isFakeData = arg_str === 'fake'
      ? true : false;
  };

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