function GroupList() {

  lastChange=0;
  
  this.loadFromLocalStorage=function() {
    if (jQuery.type(localStorage.groups)!='undefined') {
      try {
        var data=JSON.parse(localStorage.groups);
        this.lastChange=data.lastChange;
        for (el in data.groups) {
          this[el]=new Group(el,false,data.groups[el]);
        }
      }
      catch(e) {
        for (el in this) {
          if (this.isGroup(this[el])) {
            delete this[el];
          }
        }
        myconfirm('Erreur en chargeant les données locales. On les purges ?',
          function(data) {
            delete localStorage.groups;
            location.reload();
          }
        );
      }
    }
  }

  this.export=function() {
    return {
      'lastChange': this.lastChange,
      'groups': this.each(function(idx,group) {
        return group.export();
      })
    };
  }

  this.import=function(groups) {
    ret={};
    for (el in this) {
      if (this.isGroup(this[el])) {
        delete ret[el];
      }
    }
    this.lastChange=groups.lastChange;
    for (el in groups.groups) {
      this[el]=new Group(el,false,groups.groups[el]);
    }
    return true;
  }


  this.save=function() {
    localStorage.groups=JSON.stringify(this.export());
  }

  this.each=function(fct) {
    var idx=0;
    var ret={};
    for (el in this) {
      if(this.isGroup(this[el])) {
        ret[el]=fct(idx++,this[el]);
      }
    }
    return ret;
  }

  this.count=function() {
    len=0;
    this.each(function(idx,group) {
      len=len+1;
    });
    return len;
  }

  this.isGroup=function(el) {
    return (jQuery.type(el)=='object' && jQuery.type(el.isGroup)=='function' && el.isGroup());
  }

  this.byName=function(name) {
    for (el in this) {
      if(this.isGroup(this[el])) {
        if (this[el].name==name) {
          return this[el];
        }
      }
    }
    return false;
  }


  this.removeGroup=function(uuid) {
    if (this.isGroup(this[uuid])) {
      delete this[uuid];
      return true;
    }
    return false;
  }

  this.balances=function(fct) {
    return this.each(function(idx,group) {
      bal=group.balance();
      bal.name=group.name;
      return bal;
    });
  }

  this.newGroup=function(name,uuid) {
    var uuid=uuid||generate_uuid();
    this[uuid]=new Group(uuid,name);
    return uuid;
  }

}

function Group(uuid,name,data) {
  this.uuid=uuid || generate_uuid();
  this.name=name || false;
  this.contributors={};
  this.deletedContributors={};
  this.contributions={};
  this.deletedContributions={};
  this.categories={};
  this.deletedCategories={};

  this.isGroup=function() {
    return true;
  }

  this.export=function() {
    var contributors={};
    for (email in this.contributors) {
      contributors[email]=this.contributors[email].export();
    }
    var contributions={}
    for (uuid in this.contributions) {
      contributions[uuid]=this.contributions[uuid].export();
    }
    var categories={}
    for (uuid in this.categories) {
      categories[uuid]=this.categories[uuid].export();
    }
    return {
      'uuid': this.uuid,
      'name': encodeURIComponent(this.name),
      'contributors': contributors,
      'deletedContributors': this.deletedContributors,
      'contributions': contributions,
      'deletedContributions': this.deletedContributions,
      'categories': categories,
      'deletedCategories': this.deletedCategories
    };
  }

  /*
   * Contributors
   */
  this.removeContributor=function(c) {
    this.deletedContributors[c.email]=c.export();
    time=new Date().getTime();
    this.deletedContributors[c.email].deletionTime=time;
    contributions=this.contributionsByContributorEmail(c.email);
    for (idx in contributions) {
      this.deleteContribution(contributions[idx].uuid,time);
    }
    delete this.contributors[c.email];
    return true;
  }

  this.restoreContributor=function(email) {
    if (email in  this.contributors) return;
    if (! email in this.deletedContributors) return;
    this.contributors[email]=this.importContributor(this.deletedContributors[email]);
    contributions=this.deletedContributionsByContributorEmail(email);
    for (idx in contributions) {
      if (contributions[idx].lastChange==this.deletedContributors[email].deletionTime) {
        this.restoreContribution(contributions[idx].uuid);
      }
    }
    delete this.deletedContributors[email];
    return true;
  }

  this.getDeletedContributors=function() {
    var ret=[];
    for (email in this.deletedContributors) {
      ret.push(this.importContributor(this.deletedContributors[email]));
    }
    return ret;
  }

  this.contributorByName=function(name) {
    for (email in this.contributors) {
      if (this.contributors[email].name == name) return this.contributors[email];
    }
    return undefined;
  }

  this.contributorByEmail=function(email) {
    if (jQuery.type(this.contributors[email])!='undefined') {
      return this.contributors[email];
    }
    return undefined;
  }

  this.addContributor=function(c) {
    this.contributors[c.email]=c;
  }

  this.replaceContributor=function(email,c) {
    delete this.contributors[email];
    this.contributors[c.email]=c;
  }
  
  this.importContributor=function(data) {
    return new Contributor(
      decodeURIComponent(data.name),
      data.email
    );
  }

  /*
   * Contributions
   */
  this.contributionsByContributorEmail=function(email) {
    var ret=[];
    for (uuid in this.contributions) {
      if (this.contributions[uuid].contributor.email==email) {
        ret.push(this.contributions[uuid]);
      }
    }
    return this.sortContributionsByDate(ret);
  }

  this.sortContributionsByDate=function(contributions) {
    contributions.sort(function(a,b) {
      if (a.date==b.date) {
        return 0;
      }
      else if(a.date<b.date) {
        return 1;
      }
      else {
        return -1;
      }
    });
    return contributions;
  }

  this.deletedContributionsByContributorEmail=function(email) {
    var ret=[];
    for (uuid in this.deletedContributions) {
      if (this.deletedContributions[uuid].contributor==email) {
        ret.push(new Contribution(
          this.contributorByEmail(email),
          this.deletedContributions[uuid].cost,
          decodeURIComponent(this.deletedContributions[uuid].title),
          this.deletedContributions[uuid].date,
          this.deletedContributions[uuid].category,
          uuid,
          this.deletedContributions[uuid].lastChange
        ));
      }
    }
    ret.sort(function(a,b) {
      if (a.lastChange==b.lastChange) {
        return 0;
      }
      else if(a.lastChange<b.lastChange) {
        return -1;
      }
      else {
        return 1;
      }
    });
    return ret;
  }

  this.addContribution=function(c) {
    this.contributions[c.uuid]=c;
  }

  this.updateContribution=function(uuid,c) {
    c.uuid=uuid;
    this.contributions[uuid]=c;
  }

  this.deleteContribution=function(uuid,time) {
    this.contributions[uuid].lastChange=time || new Date().getTime();
    this.deletedContributions[uuid]=this.contributions[uuid].export();
    delete this.contributions[uuid];
  }

  this.restoreContribution=function(uuid) {
    this.deletedContributions[uuid].lastChange=new Date().getTime();
    this.contributions[uuid]=this.importContribution(this.deletedContributions[uuid]);
    delete this.deletedContributions[uuid];
  }

  this.importContribution=function(data) {
    return new Contribution(
      this.contributorByEmail(data.contributor),
      data.cost,
      decodeURIComponent(data.title),
      data.date,
      data.category,
      data.uuid,
      data.lastChange
    );
  }
  
  this.findContributionByTitleMatches=function() {
	var contributions=this.contributions;

    return function(q, cb) {
      var matches, substrRegex;
 
      // an array that will be populated with substring matches
      matches = [];
 
      // regex used to determine if a string contains the substring `q`
      substrRegex = new RegExp(q, 'i');

      var titles=[];
      for (uuid in contributions) {
        if (substrRegex.test(contributions[uuid].title)) {
		  var title=String(contributions[uuid].title).replace(/^\s+|\s+$/g, '');
		  if (titles.indexOf(title.toLowerCase())!=-1) {
			continue;  
	      }
	      titles.push(title.toLowerCase());
          matches.push({
			  value: title,
			  category: contributions[uuid].category
		  });
        }
      }
 
      cb(matches);
    };
  }

  this.searchContributions=function(pattern,contributor) {
    var ret=[];
    substrRegex = new RegExp(pattern, 'i');
    for (uuid in this.contributions) {
      if (contributor && contributor!=this.contributions[uuid].contributor.email) {
        continue;
      }
      if (substrRegex.test(this.contributions[uuid].title) || substrRegex.test(this.contributions[uuid].cost)) {
        ret.push(this.contributions[uuid]);
      }
    }
    return this.sortContributionsByDate(ret);
  }
  
  this.getContributionsByCategory=function (category) {
	  var ret={};
	  for (uuid in this.contributions) {
		  if (this.contributions[uuid].category==category.uuid) {
			  ret[uuid]=this.contributions[uuid];
		  }
	  }
	  return ret;
  }
  
  this.findUnusedCategories=function() {
	  var cats={};
	  for (uuid in this.contributions) {
		  cid=this.contributions[uuid].category;
		  if ($.type(cats[cid])=='undefined') {
			  cats[cid]=1;
		  }
		  else {
			  cats[cid]++;
		  }
	  }
	  var ret={};
	  for (cid in this.categories) {
		if ($.type(cats[cid])=='undefined') {
			ret[cid]=this.categories[cid];
		}
	  }
	  return ret;
  }
  
  /*
   * Categories
   */

  this.addCategory=function(c) {
    this.categories[c.uuid]=c;
  }

  this.updateCategory=function(uuid,c) {
    c.uuid=uuid;
    this.categories[uuid]=c;
  }

  this.deleteCategory=function(uuid,time) {
    this.categories[uuid].lastChange=time || new Date().getTime();
    this.deletedCategories[uuid]=this.categories[uuid].export();
    delete this.categories[uuid];
  }

  this.restoreCategory=function(uuid) {
    this.deletedCategories[uuid].lastChange=new Date().getTime();
    this.categories[uuid]=this.importCategory(this.deletedCategories[uuid]);
    delete this.deletedCategories[uuid];
  }
  
  this.getCategoryByName=function(name,approx) {
    if(approx) {
      name=String(name).replace(/^\s+|\s+$/g, '').toLowerCase();
    }
    for (uuid in this.categories) {
      if (approx) {
        if (String(this.categories[uuid].name).replace(/^\s+|\s+$/g, '').toLowerCase()==name) {
          return this.categories[uuid];
        }
      }
      else if(this.categories[uuid].name==name) {
        return this.categories[uuid]
      }
    }
    return false;
  }

  this.importCategory=function(data) {
    return new Category(
      decodeURIComponent(data.name),
      data.color,
      data.lastChange,
      data.uuid
    );
  }
  
  this.getSortedCategories=function() {
	uuids=[];
	for (uuid in this.categories) {
	  uuids.push(uuid);
	}
	tmp_cats=this.categories;
	uuids.sort(function(a,b) {
		return tmp_cats[a]['name'].localeCompare(tmp_cats[b]['name']);
	});
	var ret={};
	for (idx in uuids) {
      ret[uuids[idx]]=this.categories[uuids[idx]];
	}
	return ret;
  }
  
  this.getCategoriesStats=function() {
    var cats={};
    for (uuid in this.contributions) {
      if (jQuery.type(cats[this.contributions[uuid].category])=='undefined') {
        cats[this.contributions[uuid].category]=0;
      }
      cats[this.contributions[uuid].category]+=this.contributions[uuid].cost;
    }
    var data=[];
    for (cid in cats) {
      if (jQuery.type(this.categories[cid])!='undefined') {
        data.push({
          'label': this.categories[cid].name,
          'data': cats[cid],
          'color': this.categories[cid].color
        });
      }
    }
    return data;
  }

  /*
   * Balance
   */
  this.balance=function() {
    total={}
    min=-1;
    max=0;
    for (email in this.contributors) {
      var sum=0;
      cl=this.contributionsByContributorEmail(email);
      for (idx in cl) {
        sum+=cl[idx].cost;
      }
      if (min==-1 || min>sum) {
          min=sum;
        }
      if(max<sum) {
        max=sum;
      }
      total[email]=sum;
    }
    balance={}
    var sum=0;
    for (email in total) {
      balance[email]={
        'name': this.contributors[email].name,
        'total': total[email],
        'diff': total[email]-max,
      }
      sum=sum+total[email];
    }
    return {
      'balance': balance,
      'sum': sum,
      'min': min,
      'max': max
    };
  }

  /*
   * Contructor
   */
  if (jQuery.type(data)=='object') {
    try {
      this.uuid=data.uuid;
      this.name=data.name;
      if (jQuery.type(data.contributors) == 'object') {
        for (email in data.contributors) {
          this.contributors[email]=this.importContributor(data.contributors[email]);
        }
      }
      if (jQuery.type(data.deletedContributors) == 'object') {
        for (email in data.deletedContributors) {
          this.deletedContributors[email]=data.deletedContributors[email];
        }
      }
      if (jQuery.type(data.contributions) == 'object') {
        for (uuid in data.contributions) {
          this.contributions[uuid]=this.importContribution(data.contributions[uuid]);
        }
      }
      if (jQuery.type(data.deletedContributions) == 'object') {
        for (uuid in data.deletedContributions) {
          this.deletedContributions[uuid]=data.deletedContributions[uuid];
        }
      }
      if (jQuery.type(data.categories) == 'object') {
        for (uuid in data.categories) {
          this.categories[uuid]=this.importCategory(data.categories[uuid]);
        }
      }
      if (jQuery.type(data.deletedCategories) == 'object') {
        for (uuid in data.deletedCategories) {
          this.deletedCategories[uuid]=data.deletedCategories[uuid];
        }
      }
    }
    catch (e) {
	  console.log(e);
      alert('Une erreur est survenue en chargeant le groupe '+this.name+' depuis le cache');
    }
  }
}

function Contributor(name,email) {
  this.name=name;
  this.email=email;
  this.export=function() {
    return {
      'name': encodeURIComponent(this.name),
      'email': this.email
    };
  }
}

function Category(name,color,lastChange,uuid) {
  this.name=name;
  this.color=color || '#'+(0x1000000+(Math.random())*0xffffff).toString(16).substr(1,6);
  this.lastChange=lastChange || new Date().getTime();
  this.uuid=uuid || generate_uuid();
  this.export=function() {
    return {
      'name': encodeURIComponent(this.name),
      'color': this.color,
      'lastChange': this.lastChange,
      'uuid': this.uuid
    };
  }
}

function Contribution(contributor,cost,title,date,cat,uuid,lastChange) {
  this.contributor=contributor;
  this.cost=cost;
  this.title=title;
  if (jQuery.type(date)!='date') {
    date=moment(date).toDate();
  }
  this.date=date;
  this.uuid=uuid || generate_uuid();
  this.category=cat;
  this.lastChange=lastChange || new Date().getTime();
  this.export=function() {
    return {
      'contributor': this.contributor.email,
      'uuid': this.uuid,
      'cost': this.cost,
      'title': encodeURIComponent(this.title),
      'date': this.date.getTime(),
      'category': this.category,
      'lastChange': this.lastChange,
    };
  }

  this.getTitle=function() {
    if (jQuery.type(this.title)=='string') {
      return this.title;
    }
    return '';
  }
}

function SyncServer() {
  this.url=false;
  this.email=false;
  this.password=false;
  this.logged=false;

  this.login=function(url,email,password,onsuccess,onerror) {
    this.url=url;
    this.email=email;
    this.password=password;

    try {
      jQuery.getJSON(
        this.url+'/login',
        {'email':email,'password':password, 'time': new Date().getTime()},
        function(data, textStatus) {
          console.log(data);
          if (textStatus=='success') {
            if(jQuery.type(data.email) != 'undefined' && jQuery.type(data.name) != 'undefined') {
              console.log('Login success return');
              console.log(onsuccess);
              onsuccess(data);
              return true;
            }
          }
          onerror(data);
        }
      ).fail(onerror);
    }
    catch(e) {
      if(jQuery.type(onerror)=='function') {
        onerror();
      }
    }
  }

  this.subscribe=function(url,email,name,password,onsuccess,onerror) {
    this.url=url;
    this.email=email;
    this.name=name;
    this.password=password;

    try {
      jQuery.getJSON(
        this.url+'/subscribe',
        {'email':email,'name': name,'password':password, 'time': new Date().getTime()},
        function(data, textStatus) {
          console.log(data);
          if (textStatus=='success') {
            if(jQuery.type(data.email) != 'undefined' && jQuery.type(data.name) != 'undefined') {
              onsuccess(data);
              return true;
            }
          }
          onerror(data);
          return false;
        }
      ).fail(onerror);
    }
    catch(e) {
      if(jQuery.type(onerror)=='function') {
        onerror();
      }
    }
  }


  this.sync=function(url,email,password,groups,onsuccess,onerror) {
    this.url=url;
    this.email=email;
    this.password=password;
    try {
      jQuery.post(
        this.url+'/sync',
        {
          'email':email,
          'password':password,
          'groups': JSON.stringify(groups)
        },
        function(data, textStatus) {
          console.log(data);
          if (textStatus=='success') {
            if(jQuery.type(data.groups)) {
              console.log('Sync success return');
              onsuccess(data);
              return true;
            }
          }
          onerror(data);
        },
        'json'
      ).fail(onerror);
    }
    catch(e) {
      if(jQuery.type(onerror)=='function') {
        onerror();
      }
    }
  }

}
