package com.planet_ink.coffee_mud.Common;
import com.planet_ink.coffee_mud.core.interfaces.*;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;
import com.planet_ink.coffee_mud.Abilities.interfaces.*;
import com.planet_ink.coffee_mud.Areas.interfaces.*;
import com.planet_ink.coffee_mud.Behaviors.interfaces.*;
import com.planet_ink.coffee_mud.CharClasses.interfaces.*;
import com.planet_ink.coffee_mud.Commands.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.*;
import com.planet_ink.coffee_mud.Common.interfaces.AccountStats.Agent;
import com.planet_ink.coffee_mud.Common.interfaces.TimeClock.TimePeriod;
import com.planet_ink.coffee_mud.Exits.interfaces.*;
import com.planet_ink.coffee_mud.Items.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.*;
import com.planet_ink.coffee_mud.Libraries.interfaces.AbilityMapper.AbilityMapping;
import com.planet_ink.coffee_mud.Libraries.interfaces.AchievementLibrary.Achievement;
import com.planet_ink.coffee_mud.Libraries.interfaces.AchievementLibrary.Tracker;
import com.planet_ink.coffee_mud.Libraries.interfaces.XMLLibrary.XMLTag;
import com.planet_ink.coffee_mud.Locales.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.StdMOB;
import com.planet_ink.coffee_mud.MOBS.interfaces.*;
import com.planet_ink.coffee_mud.MOBS.interfaces.MOB.Attrib;
import com.planet_ink.coffee_mud.Races.interfaces.*;

import java.nio.ByteBuffer;
import java.util.*;

/*
   Copyright 2010-2022 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
public class DefaultPlayerAccount implements PlayerAccount
{
	@Override
	public String ID()
	{
		return "DefaultPlayerAccount";
	}

	@Override
	public String name()
	{
		return ID();
	}

	protected SHashSet<String>	friends				= new SHashSet<String>();
	protected SHashSet<String>	ignored				= new SHashSet<String>();
	protected SHashSet<String>	subscriptions		= new SHashSet<String>();
	protected SVector<String>	players				= new SVector<String>();
	protected String			accountName 		= "";
	protected String			lastIP				= "";
	protected long				lastDateTime		= System.currentTimeMillis();
	protected long				lastUpdated			= 0;
	protected String			email				= "";
	protected String			password			= "";
	protected String			notes				= "";
	protected long 				accountExpiration	= 0;
	protected int				bonusCommonSk		= 0;
	protected int				bonusCraftSk		= 0;
	protected int				bonusNonCraftSk		= 0;
	protected int				bonusLanguages		= 0;
	protected int				bonusCharStatPt		= 0;
	protected int				bonusCharLimit		= 0;
	protected int				bonusCharOnlineLimit= 0;
	protected String[]			xtraValues			= null;
	protected Set<AccountFlag>	acctFlags			= new SHashSet<AccountFlag>();
	protected volatile MOB 		fakePlayerM			= null;
	protected long[]			prideExpireTime		= new long[TimeClock.TimePeriod.values().length];
	protected int[][]			prideStats			= new int[TimeClock.TimePeriod.values().length][AccountStats.PrideStat.values().length];

	protected SVector<PlayerLibrary.ThinPlayer> thinPlayers 	= new SVector<PlayerLibrary.ThinPlayer>();
	protected Map<String,Tracker>				achievementers	= new STreeMap<String,Tracker>();
	protected CMUniqNameSortSVec<Tattoo>		tattoos			= new CMUniqNameSortSVec<Tattoo>(1);

	public DefaultPlayerAccount()
	{
		super();
		xtraValues=CMProps.getExtraStatCodesHolder(this);
	}

	@Override
	public CMObject newInstance()
	{
		try
		{
			return getClass().newInstance();
		}
		catch(final Exception e)
		{
			return new DefaultPlayerStats();
		}
	}

	@Override
	public void initializeClass()
	{
	}

	@Override
	public CMObject copyOf()
	{
		try
		{
			final DefaultPlayerAccount O=(DefaultPlayerAccount)this.clone();
			O.friends=friends.copyOf();
			O.subscriptions=subscriptions.copyOf();
			O.ignored=ignored.copyOf();
			O.xtraValues=(xtraValues==null)?null:(String[])xtraValues.clone();
			O.thinPlayers = thinPlayers.copyOf();
			O.achievementers = new STreeMap<String,Tracker>();
			for(final String key : achievementers.keySet())
				O.achievementers.put(key, achievementers.get(key).copyOf());
			O.tattoos = new CMUniqNameSortSVec<Tattoo>(tattoos);
			O.acctFlags = new SHashSet<AccountFlag>(acctFlags);
			O.fakePlayerM = null;
			return O;
		}
		catch(final CloneNotSupportedException e)
		{
			return new DefaultPlayerStats();
		}
	}

	@Override
	public void copyInto(final PlayerAccount otherAccount)
	{
		for(final String stat : this.getStatCodes())
			otherAccount.setStat(stat, this.getStat(stat));
		if(otherAccount instanceof DefaultPlayerAccount)
		{
			final DefaultPlayerAccount O = (DefaultPlayerAccount)otherAccount;
			O.friends=friends.copyOf();
			O.subscriptions=subscriptions.copyOf();
			O.ignored=ignored.copyOf();
			O.xtraValues=(xtraValues==null)?null:(String[])xtraValues.clone();
			O.thinPlayers = thinPlayers.copyOf();
			O.achievementers = new STreeMap<String,Tracker>(achievementers);
			O.tattoos = new CMUniqNameSortSVec<Tattoo>(tattoos);
			O.acctFlags = new SHashSet<AccountFlag>(acctFlags);
			O.fakePlayerM = null;
			O.accountName = accountName;
			O.lastIP = lastIP;
			O.lastDateTime = lastDateTime;
			O.lastUpdated = lastUpdated;
			O.email = email;
			O.password = password;
			O.notes = notes;
			O.accountExpiration = accountExpiration;
			O.bonusCommonSk = bonusCommonSk;
			O.bonusCraftSk = bonusCraftSk;
			O.bonusNonCraftSk = bonusNonCraftSk;
			O.bonusLanguages = bonusLanguages;
			O.bonusCharStatPt = bonusCharStatPt;
			O.bonusCharLimit = bonusCharLimit;
			O.bonusCharOnlineLimit= bonusCharOnlineLimit;
		}
	}

	@Override
	public String getLastIP()
	{
		return lastIP;
	}

	@Override
	public void setLastIP(final String ip)
	{
		lastIP=ip;
	}

	@Override
	public String getEmail()
	{
		if(email==null)
			return "";
		return email;
	}

	@Override
	public void setEmail(final String newAdd)
	{
		email=newAdd;
	}

	@Override
	public long getLastUpdated()
	{
		return lastUpdated;
	}

	@Override
	public void setLastUpdated(final long time)
	{
		lastUpdated=time;
	}

	@Override
	public long getLastDateTime()
	{
		return lastDateTime;
	}

	@Override
	public void setLastDateTime(final long C)
	{
		lastDateTime=C;
	}

	@Override
	public String getPasswordStr()
	{
		return password;
	}

	@Override
	public void setPassword(final String newPassword)
	{
		if(CMProps.getBoolVar(CMProps.Bool.HASHPASSWORDS)
		&&(!CMLib.encoder().isARandomHashString(newPassword)))
			password=CMLib.encoder().makeRandomHashString(newPassword);
		else
			password=newPassword;
	}

	@Override
	public boolean matchesPassword(final String checkPass)
	{
		return CMLib.encoder().passwordCheck(checkPass, password);
	}

	@Override
	public String getNotes()
	{
		return notes;
	}

	@Override
	public void setNotes(final String newnotes)
	{
		notes=newnotes;
	}

	protected SHashSet<String> getHashFrom(String str, final boolean trim)
	{
		final SHashSet<String> h=new SHashSet<String>();
		int x=str.indexOf(';');
		while(x>=0)
		{
			final String fi=trim?str.substring(0,x).trim():str.substring(0,x);
			if(fi.length()>0)
				h.add(fi);
			str=str.substring(x+1);
			x=str.indexOf(';');
		}
		if(str.trim().length()>0)
			h.add(trim?str.trim():str);
		return h;
	}

	@Override
	public Set<String> getFriends()
	{
		return friends;
	}

	@Override
	public Set<String> getIgnored()
	{
		return ignored;
	}

	@Override
	public Set<String> getSubscriptions()
	{
		return subscriptions;
	}

	@Override
	public boolean isIgnored(MOB mob)
	{
		if(mob==null)
			return false;
		if (mob.soulMate() != null)
			mob=mob.soulMate();
		if(ignored.contains(mob.Name()))
			return true;
		final PlayerStats stats=mob.playerStats();
		if(stats ==null)
			return false;
		final PlayerAccount account=stats.getAccount();
		if(account == null)
			return false;
		return ignored.contains(account.getAccountName()+"*");
	}

	@Override
	public boolean isIgnored(final String name)
	{
		if(name==null)
			return false;
		return (ignored.contains(name) || ignored.contains(name+"*"));
	}

	@Override
	public void bumpPrideStat(final PrideStat stat, final int amt)
	{
		final long now=System.currentTimeMillis();
		if(stat==null)
			return;
		for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
		{
			if(period==TimeClock.TimePeriod.ALLTIME)
				prideStats[period.ordinal()][stat.ordinal()]+=amt;
			else
			{
				if(now>prideExpireTime[period.ordinal()])
				{
					for(final AccountStats.PrideStat stat2 : AccountStats.PrideStat.values())
						prideStats[period.ordinal()][stat2.ordinal()]=0;
					prideExpireTime[period.ordinal()]=period.nextPeriod();
				}
				prideStats[period.ordinal()][stat.ordinal()]+=amt;
			}
		}
	}

	@Override
	public int getPrideStat(final TimePeriod period, final PrideStat stat)
	{
		if((period==null)||(stat==null))
			return 0;
		return prideStats[period.ordinal()][stat.ordinal()];
	}

	@Override
	public MOB getAccountMob()
	{
		if(fakePlayerM!=null)
			return fakePlayerM;
		synchronized(this)
		{
			if(fakePlayerM!=null)
				return fakePlayerM;
			fakePlayerM=CMClass.getMOB("StdMOB");
			fakePlayerM.setName(getAccountName());
			fakePlayerM.setPlayerStats((PlayerStats)CMClass.getCommon("DefaultPlayerStats"));
			fakePlayerM.playerStats().setAccount(this);
			fakePlayerM.playerStats().setEmail(getEmail());
			fakePlayerM.basePhyStats().setLevel(1);
			fakePlayerM.recoverPhyStats();
			return fakePlayerM;
		}
	}

	protected String getPrivateList(final Set<String> h)
	{
		if((h==null)||(h.size()==0))
			return "";
		final StringBuffer list=new StringBuffer("");
		for (final String string : h)
			list.append((string)+";");
		return list.toString();
	}

	@Override
	public String getXML()
	{
		final StringBuffer rest=new StringBuffer("");
		final String[] codes=getStatCodes();
		final XMLLibrary libXML = CMLib.xml();
		for (final String code2 : codes)
		{
			final String code=code2.toUpperCase();
			final String value = getStat(code);
			if(value.length()==0)
				rest.append("<"+code+" />");
			else
				rest.append("<"+code+">"+libXML.parseOutAngleBrackets(value)+"</"+code+">");
		}
		rest.append("<NEXTPRIDEPERIODS>").append(CMParms.toTightListString(prideExpireTime)).append("</NEXTPRIDEPERIODS>");
		rest.append("<PRIDESTATS>");
		for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
			rest.append(CMParms.toTightListString(prideStats[period.ordinal()])).append(";");
		rest.append("</PRIDESTATS>");
		rest.append("<ACHIEVEMENTS");
		for(final Iterator<Tracker> i=achievementers.values().iterator();i.hasNext();)
		{
			final Tracker T = i.next();
			if(T.getAchievement().isSavableTracker() && (T.getCount(null) != 0))
				rest.append(" ").append(T.getAchievement().getTattoo()).append("=").append(T.getCount(null));
			// getCount(null) should be ok, because it's only the un-savable trackers that need the mob obj
		}
		rest.append(" />");
		rest.append("<TATTOOS>").append(CMParms.toListString(tattoos)).append("</TATTOOS>");
		rest.append("<ACCSTATS>")
			.append(bonusCommonSk).append(';')
			.append(bonusCraftSk).append(';')
			.append(bonusNonCraftSk).append(';')
			.append(bonusLanguages).append(';')
			.append(bonusCharStatPt).append(';')
			.append(bonusCharLimit).append(';')
			.append(bonusCharOnlineLimit).append(';')
			.append("</ACCSTATS>");
		return rest.toString();
	}

	@Override
	public void setXML(final String str)
	{
		final XMLLibrary xmlLib = CMLib.xml();
		final List<XMLLibrary.XMLTag> xml = xmlLib.parseAllXML(str);
		final String[] codes=getStatCodes();
		for (final String code : codes)
		{
			String val=xmlLib.getValFromPieces(xml,code.toUpperCase());
			if(val==null)
				val="";
			setStat(code.toUpperCase(),xmlLib.restoreAngleBrackets(val));
		}
		final String[] nextPeriods=xmlLib.getValFromPieces(xml, "NEXTPRIDEPERIODS").split(",");
		final String[] prideStats=xmlLib.getValFromPieces(xml, "PRIDESTATS").split(";");
		final Pair<Long,int[]>[] finalPrideStats = CMLib.players().parsePrideStats(nextPeriods, prideStats);
		for(final TimeClock.TimePeriod period : TimeClock.TimePeriod.values())
		{
			if(period.ordinal()<finalPrideStats.length)
			{
				this.prideExpireTime[period.ordinal()]=finalPrideStats[period.ordinal()].first.longValue();
				this.prideStats[period.ordinal()]=finalPrideStats[period.ordinal()].second;
			}
		}
		final XMLTag achievePiece = xmlLib.getPieceFromPieces(xml, "ACHIEVEMENTS");
		achievementers.clear();
		for(final Enumeration<Achievement> a=CMLib.achievements().achievements(Agent.ACCOUNT);a.hasMoreElements();)
		{
			final Achievement A=a.nextElement();
			if((achievePiece != null) && achievePiece.parms().containsKey(A.getTattoo()))
				achievementers.put(A.getTattoo(), A.getTracker(CMath.s_int(achievePiece.parms().get(A.getTattoo()).trim())));
			else
				achievementers.put(A.getTattoo(), A.getTracker(0));
		}
		final String[] allTattoos=xmlLib.getValFromPieces(xml, "TATTOOS").split(",");
		this.tattoos.clear();
		for(final String tattoo : allTattoos)
			this.addTattoo(tattoo);

		final String[] allAccStats=xmlLib.getValFromPieces(xml, "ACCSTATS").split(";");
		if(allAccStats.length>=7)
		{
			bonusCommonSk=CMath.s_int(allAccStats[0]);
			bonusCraftSk=CMath.s_int(allAccStats[1]);
			bonusNonCraftSk=CMath.s_int(allAccStats[2]);
			bonusLanguages=CMath.s_int(allAccStats[3]);
			bonusCharStatPt=CMath.s_int(allAccStats[4]);
			bonusCharLimit=CMath.s_int(allAccStats[5]);
			bonusCharOnlineLimit=CMath.s_int(allAccStats[6]);
		}
	}

	// Acct Expire Code
	@Override
	public long getAccountExpiration()
	{
		return accountExpiration;
	}

	@Override
	public void setAccountExpiration(final long newVal)
	{
		accountExpiration=newVal;
	}

	@Override
	public String getAccountName()
	{
		return accountName;
	}

	@Override
	public void setAccountName(final String name)
	{
		accountName = name;
	}

	@Override
	public void addNewPlayer(final MOB mob)
	{
		if(players.contains(mob.Name()))
			return;
		if(mob==fakePlayerM)
			return;
		try
		{
			for(final String name : players)
			{
				if(name.equalsIgnoreCase(mob.Name()))
					return;
			}
		}
		catch(final Exception e)
		{
		}
		CMLib.achievements().possiblyBumpAchievement(mob, AchievementLibrary.Event.CHARACTERS, 1);
		players.add(mob.Name());
		thinPlayers.clear();
	}

	@Override
	public String findPlayer(final String name)
	{
		if(name==null)
			return null;
		for(final String pName : players)
		{
			if(pName.equalsIgnoreCase(name))
				return pName;
		}
		return null;
	}

	@Override
	public void delPlayer(final String name)
	{
		players.remove(name);
		try
		{
			for(final String name1 : players)
			{
				if(name1.equalsIgnoreCase(name))
					players.remove(name1);
			}
			CMLib.achievements().possiblyBumpAchievement(null, AchievementLibrary.Event.CHARACTERS, -1);
		}
		catch(final Exception e)
		{
		}
		thinPlayers.clear();
	}

	@Override
	public void delPlayer(final MOB mob)
	{
		if(mob==fakePlayerM)
			return;
		players.remove(mob.Name());
		try
		{
			for(final String name : players)
			{
				if(name.equalsIgnoreCase(mob.Name()))
					players.remove(name);
			}
		}
		catch(final Exception e)
		{
		}
		CMLib.achievements().possiblyBumpAchievement(mob, AchievementLibrary.Event.CHARACTERS, -1);
		thinPlayers.clear();
	}

	@Override
	public Enumeration<MOB> getLoadPlayers()
	{
		final Vector<MOB> mobs = new Vector<MOB>(players.size());
		for(final Enumeration<String> e=getPlayers();e.hasMoreElements();)
		{
			final MOB M=CMLib.players().getLoadPlayer(e.nextElement());
			if(M!=null)
				mobs.addElement(M);
		}
		return mobs.elements();
	}

	@Override
	public Enumeration<PlayerLibrary.ThinPlayer> getThinPlayers()
	{
		synchronized(thinPlayers)
		{
			if(thinPlayers.size() != players.size())
			{
				for(final Enumeration<String> e=getPlayers();e.hasMoreElements();)
				{
					final String name = e.nextElement();
					PlayerLibrary.ThinPlayer tP = CMLib.database().getThinUser(name);
					if (tP == null)
					{
						tP = new PlayerLibrary.ThinPlayer()
						{
							@Override
							public String name()
							{
								return name;
							}

							@Override
							public String charClass()
							{
								return "";
							}

							@Override
							public String race()
							{
								return "";
							}

							@Override
							public int level()
							{
								return 0;
							}

							@Override
							public int age()
							{
								return 0;
							}

							@Override
							public long last()
							{
								return 0;
							}

							@Override
							public String email()
							{
								return null;
							}

							@Override
							public String ip()
							{
								return "";
							}

							@Override
							public int exp()
							{
								return 0;
							}

							@Override
							public int expLvl()
							{
								return 0;
							}

							@Override
							public String liege()
							{
								return "";
							}

							@Override
							public String worship()
							{
								return "";
							}
						};
					}
					thinPlayers.add(tP);
				}
			}
		}
		return thinPlayers.elements();
	}

	@Override
	public Enumeration<String> getPlayers()
	{
		return players.elements();
	}

	@Override
	public void setPlayerNames(final List<String> names)
	{
		if(names != null)
		{
			players = new SVector<String>(names);
			for(final String name : players)
			{
				final MOB M=CMLib.players().getPlayer(name);
				if((M!=null)&&(M.playerStats()!=null)&&(M.playerStats().getAccount()==null))
					M.playerStats().setAccount(this);
			}
		}
	}

	@Override
	public int numPlayers()
	{
		return players.size();
	}

	@Override
	public boolean isSet(final AccountFlag flag)
	{
		return acctFlags.contains(flag);
	}

	@Override
	public void setFlag(final AccountFlag flag, final boolean setOrUnset)
	{
		if(setOrUnset)
			acctFlags.add(flag);
		else
			acctFlags.remove(flag);
	}

	@Override
	public void killAchievementTracker(final Achievement A, final Tattooable tracked, final MOB mob)
	{
		if(achievementers.containsKey(A.getTattoo()))
		{
			achievementers.remove(A.getTattoo());
			if(mob != null)
			{
				mob.delTattoo(A.getTattoo());
			}
		}
	}

	@Override
	public Tracker getAchievementTracker(final Achievement A, final Tattooable tracked, final MOB mob)
	{
		final Tracker T;
		if(achievementers.containsKey(A.getTattoo()))
		{
			T=achievementers.get(A.getTattoo());
		}
		else
		{
			T=A.getTracker(0);
			achievementers.put(A.getTattoo(), T);
		}
		return T;
	}

	@Override
	public void rebuildAchievementTracker(final Tattooable tracked, final MOB mob, final String achievementTattoo)
	{
		final Achievement A=CMLib.achievements().getAchievement(achievementTattoo);
		if(A!=null)
		{
			if(achievementers.containsKey(A.getTattoo()))
				achievementers.put(A.getTattoo(), A.getTracker(achievementers.get(A.getTattoo()).getCount(mob)));
			else
				achievementers.put(A.getTattoo(), A.getTracker(0));
		}
		else
			achievementers.remove(achievementTattoo);
	}

	/** Manipulation of the tatoo list */
	@Override
	public void addTattoo(final String of)
	{
		final Tattoo T=(Tattoo)CMClass.getCommon("DefaultTattoo");
		addTattoo(T.set(of));
	}

	@Override
	public void addTattoo(final String of, final int tickDown)
	{
		final Tattoo T=(Tattoo)CMClass.getCommon("DefaultTattoo");
		addTattoo(T.set(of,tickDown));
	}

	@Override
	public void delTattoo(final String of)
	{
		final Tattoo T=findTattoo(of);
		if(T!=null)
			tattoos.remove(T);
	}

	@Override
	public void addTattoo(final Tattoo of)
	{
		if ((of == null) || (of.getTattooName() == null) || (of.getTattooName().length() == 0) || findTattoo(of.getTattooName()) != null)
			return;
		tattoos.addElement(of);
	}

	@Override
	public void delTattoo(final Tattoo of)
	{
		if ((of == null) || (of.getTattooName() == null) || (of.getTattooName().length() == 0))
			return;
		final Tattoo tat = findTattoo(of.getTattooName());
		if (tat == null)
			return;
		tattoos.remove(tat);
	}

	@Override
	public Enumeration<Tattoo> tattoos()
	{
		return tattoos.elements();
	}

	@Override
	public Tattoo findTattoo(final String of)
	{
		if ((of == null) || (of.length() == 0))
			return null;
		return tattoos.find(of.trim());
	}

	@Override
	public Tattoo findTattooStartsWith(final String of)
	{
		if ((of == null) || (of.length() == 0))
			return null;
		return tattoos.findStartsWith(of.trim());
	}

	@Override
	public int getBonusCharStatPoints()
	{
		return this.bonusCharStatPt;
	}

	@Override
	public void setBonusCharStatPoints(final int bonus)
	{
		this.bonusCharStatPt = bonus;
	}

	@Override
	public int getBonusCommonSkillLimits()
	{
		return this.bonusCommonSk;
	}

	@Override
	public void setBonusCommonSkillLimits(final int bonus)
	{
		this.bonusCommonSk = bonus;
	}

	@Override
	public int getBonusCraftingSkillLimits()
	{
		return this.bonusCraftSk;
	}

	@Override
	public void setBonusCraftingSkillLimits(final int bonus)
	{
		this.bonusCraftSk = bonus;
	}

	@Override
	public int getBonusNonCraftingSkillLimits()
	{
		return this.bonusNonCraftSk;
	}

	@Override
	public void setBonusNonCraftingSkillLimits(final int bonus)
	{
		this.bonusNonCraftSk = bonus;
	}

	@Override
	public int getBonusLanguageLimits()
	{
		return this.bonusLanguages;
	}

	@Override
	public void setBonusLanguageLimits(final int bonus)
	{
		this.bonusLanguages = bonus;
	}

	@Override
	public int getBonusCharsOnlineLimit()
	{
		return bonusCharOnlineLimit;
	}

	@Override
	public void setBonusCharsOnlineLimit(final int bonus)
	{
		bonusCharOnlineLimit = bonus;
	}

	@Override
	public int getBonusCharsLimit()
	{
		return bonusCharLimit;
	}

	@Override
	public void setBonusCharsLimit(final int bonus)
	{
		bonusCharLimit = bonus;
	}

	protected static String[] CODES={"CLASS","FRIENDS","IGNORE","LASTIP","LASTDATETIME",
									 "NOTES","ACCTEXPIRATION","FLAGS","EMAIL",
									 "BONUSCOMMON", "BONUSCRAFT","BONUSNONCRAFT","BONUSLANGS",
									 "BONUSCHARSTATS", "BONUSCHARLIMIT", "BONUSCHARONLINE",
									 "SUBSCRIPTIONS"};

	@Override
	public String getStat(final String code)
	{
		switch(getCodeNum(code))
		{
		case 0:
			return ID();
		case 1:
			return getPrivateList(getFriends());
		case 2:
			return getPrivateList(getIgnored());
		case 3:
			return lastIP;
		case 4:
			return "" + lastDateTime;
		case 5:
			return notes;
		case 6:
			return "" + accountExpiration;
		case 7:
			return CMParms.toListString(acctFlags);
		case 8:
			return email;
		case 9:
			return "" + bonusCommonSk;
		case 10:
			return "" + bonusCraftSk;
		case 11:
			return "" + bonusNonCraftSk;
		case 12:
			return "" + bonusLanguages;
		case 13:
			return "" + bonusCharStatPt;
		case 14:
			return "" + bonusCharLimit;
		case 15:
			return "" + bonusCharOnlineLimit;
		case 16:
			return getPrivateList(getSubscriptions());
		default:
			return CMProps.getStatCodeExtensionValue(getStatCodes(), xtraValues, code);
		}
	}

	@Override
	public void setStat(final String code, final String val)
	{
		switch(getCodeNum(code))
		{
		case 0:
			break;
		case 1:
		{
			friends.clear();
			friends.addAll(getHashFrom(val,true));
			break;
		}
		case 2:
		{
			ignored.clear();
			ignored.addAll(getHashFrom(val,true));
			break;
		}
		case 3:
			lastIP = val;
			break;
		case 4:
			lastDateTime = CMath.s_long(val);
			break;
		case 5:
			notes = val;
			break;
		case 6:
			accountExpiration = CMath.s_long(val);
			break;
		case 7:
		{
			acctFlags = new SHashSet<AccountFlag>();
			for(final String s : CMParms.parseCommas(val.toUpperCase(),true))
			{
				final AccountFlag flag = (AccountFlag)CMath.s_valueOf(AccountFlag.class, s);
				if(flag != null)
					acctFlags.add(flag);
			}
			break;
		}
		case 8:
			email = val;
			break;
		case 9:
			bonusCommonSk = CMath.s_parseIntExpression(val);
			break;
		case 10:
			bonusCraftSk = CMath.s_parseIntExpression(val);
			break;
		case 11:
			bonusNonCraftSk = CMath.s_parseIntExpression(val);
			break;
		case 12:
			bonusLanguages = CMath.s_parseIntExpression(val);
			break;
		case 13:
			bonusCharStatPt = CMath.s_parseIntExpression(val);
			break;
		case 14:
			bonusCharLimit = CMath.s_parseIntExpression(val);
			break;
		case 15:
			bonusCharOnlineLimit = CMath.s_parseIntExpression(val);
			break;
		case 16:
		{
			subscriptions.clear();
			subscriptions.addAll(getHashFrom(val,false));
			final List<String> addThese=new ArrayList<String>();
			for(final String s : subscriptions)
			{
				if((s.length()>3)
				&&(Character.isLetter(s.charAt(0)))
				&&(s.charAt(1)==' ')
				&&(s.charAt(2)==':'))
					addThese.add(s);
			}
			for(final String s : addThese)
			{
				subscriptions.remove(s);
				subscriptions.add(" "+s);
			}
			break;
		}
		default:
			CMProps.setStatCodeExtensionValue(getStatCodes(), xtraValues, code, val);
			break;
		}
	}

	@Override
	public int getSaveStatIndex()
	{
		return (xtraValues == null) ? getStatCodes().length : getStatCodes().length - xtraValues.length;
	}

	private static String[]	codes	= null;

	@Override
	public String[] getStatCodes()
	{
		if (codes == null)
			codes = CMProps.getStatCodesList(CODES, this);
		return codes;
	}

	@Override
	public boolean isStat(final String code)
	{
		return CMParms.indexOf(getStatCodes(), code.toUpperCase().trim()) >= 0;
	}

	protected int getCodeNum(final String code)
	{
		for (int i = 0; i < CODES.length; i++)
		{
			if (code.equalsIgnoreCase(CODES[i]))
				return i;
		}
		return -1;
	}

	public boolean sameAs(final PlayerAccount E)
	{
		if (!(E instanceof DefaultPlayerAccount))
			return false;
		for (int i = 0; i < getStatCodes().length; i++)
		{
			if (!E.getStat(getStatCodes()[i]).equals(getStat(getStatCodes()[i])))
				return false;
		}
		return true;
	}

	@Override
	public int compareTo(final CMObject o)
	{
		return CMClass.classID(this).compareToIgnoreCase(CMClass.classID(o));
	}

}
