package qora.voting;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;

import database.DBSet;
import qora.account.Account;
import qora.crypto.Base58;
import utils.Pair;

public class Poll 
{
	private static final int CREATOR_LENGTH = 25;
	private static final int NAME_SIZE_LENGTH = 4;
	private static final int DESCRIPTION_SIZE_LENGTH = 4;
	private static final int OPTIONS_SIZE_LENGTH = 4;
	
	private Account creator;
	private String name;
	private String description;
	private List<PollOption> options;
	
	public Poll(Account creator, String name, String description, List<PollOption> options)
	{
		this.creator = creator;
		this.name = name;
		this.description = description;
		this.options = options;
	}
	
	//GETTERS/SETTERS

	public Account getCreator() {
		return this.creator;
	}

	public String getName() {
		return this.name;
	}

	public String getDescription() {
		return this.description;
	}
	
	public List<PollOption> getOptions() {
		return this.options;
	}
	
	public boolean isConfirmed()
	{
		return DBSet.getInstance().getPollMap().contains(this);
	}
	
	public boolean hasVotes()
	{
		for(PollOption option: this.options)
		{
			if(option.getVoters().size() > 0)
			{
				return true;
			}
		}
		
		return false;
	}
	
	public BigDecimal getTotalVotes() 
	{
		BigDecimal votes = BigDecimal.ZERO.setScale(8);
		
		for(PollOption option: this.options)
		{
			votes = votes.add(option.getVotes());
		}
		
		return votes;
	}
	
	public List<Pair<Account, PollOption>> getVotes() 
	{
		List<Pair<Account, PollOption>> votes = new ArrayList<Pair<Account, PollOption>>();
		
		for(PollOption option: this.options)
		{
			for(Account voter: option.getVoters())
			{
				Pair<Account, PollOption> vote = new Pair<Account, PollOption>(voter, option);
				votes.add(vote);
			}
		}
		
		return votes;
	}
	
	public List<Pair<Account, PollOption>> getVotes(List<Account> accounts)
	{
		List<Pair<Account, PollOption>> votes = new ArrayList<Pair<Account, PollOption>>();
		
		for(PollOption option: this.options)
		{
			for(Account voter: option.getVoters())
			{
				if(accounts.contains(voter))
				{
					Pair<Account, PollOption> vote = new Pair<Account, PollOption>(voter, option);
					votes.add(vote);
				}
			}
		}
		
		return votes;
	}
	
	public PollOption getOption(String option)
	{
		for(PollOption pollOption: this.options)
		{
			if(pollOption.getName().equals(option))
			{
				return pollOption;
			}
		}
		
		return null;
	}
	
	//PARSE
	
	public static Poll parse(byte[] data) throws Exception
	{
		int position = 0;
		
		//READ CREATOR
		byte[] creatorBytes = Arrays.copyOfRange(data, position, position + CREATOR_LENGTH);
		Account creator = new Account(Base58.encode(creatorBytes));
		position += CREATOR_LENGTH;
		
		//READ NAME SIZE
		byte[] nameLengthBytes = Arrays.copyOfRange(data, position, position + NAME_SIZE_LENGTH);
		int nameLength = Ints.fromByteArray(nameLengthBytes);
		position += NAME_SIZE_LENGTH;
				
		if(nameLength < 1 || nameLength > 400)
		{
			throw new Exception("Invalid name length");
		}
		
		//READ NAME
		byte[] nameBytes = Arrays.copyOfRange(data, position, position + nameLength);
		String name = new String(nameBytes, StandardCharsets.UTF_8);
		position += nameLength;
		
		//READ DESCRIPTION
		byte[] descriptionLengthBytes = Arrays.copyOfRange(data, position, position + DESCRIPTION_SIZE_LENGTH);
		int descriptionLength = Ints.fromByteArray(descriptionLengthBytes);
		position += DESCRIPTION_SIZE_LENGTH;
				
		if(descriptionLength < 1 || descriptionLength > 4000)
		{
			throw new Exception("Invalid description length");
		}
				
		byte[] descriptionBytes = Arrays.copyOfRange(data, position, position + descriptionLength);
		String description = new String(descriptionBytes, StandardCharsets.UTF_8);
		position += descriptionLength;
		
		//READ OPTIONS SIZE
		byte[] optionsLengthBytes = Arrays.copyOfRange(data, position, position + OPTIONS_SIZE_LENGTH);
		int optionsLength = Ints.fromByteArray(optionsLengthBytes);
		position += OPTIONS_SIZE_LENGTH;
		
		if(optionsLength < 1 || optionsLength > 100)
		{
			throw new Exception("Invalid options length");
		}
		
		//READ OPTIONS
		List<PollOption> options = new ArrayList<PollOption>();
		for(int i=0; i<optionsLength; i++)
		{
			PollOption option = PollOption.parse(Arrays.copyOfRange(data, position, data.length));
			position += option.getDataLength();
			options.add(option);
		}
		
		return new Poll(creator, name, description, options);
	}
	
	@SuppressWarnings("unchecked")
	public JSONObject toJson() 
	{
		//GET BASE
		JSONObject poll = new JSONObject();
										
		//ADD NAME/DESCRIPTIONS/OPTIONS
		poll.put("creator", this.getCreator().getAddress());
		poll.put("name", this.getName());
		poll.put("description", this.getDescription());
		
		
		JSONArray jsonOptions = new JSONArray();
		for(PollOption option: this.options)
		{
			jsonOptions.add(option.toJson());
		}
		poll.put("options", jsonOptions);
										
		return poll;	
	}	
	
	public byte[] toBytes()
	{
		byte[] data = new byte[0];
		
		//WRITE CREATOR
		try
		{
			data = Bytes.concat(data , Base58.decode(this.creator.getAddress()));
		}
		catch(Exception e)
		{
			//DECODE EXCEPTION
		}
		
		//WRITE NAME SIZE
		byte[] nameBytes = this.name.getBytes(StandardCharsets.UTF_8);
		int nameLength = nameBytes.length;
		byte[] nameLengthBytes = Ints.toByteArray(nameLength);
		data = Bytes.concat(data, nameLengthBytes);
		
		//WRITE NAME
		data = Bytes.concat(data, nameBytes);
		
		//WRITE DESCRIPTION SIZE
		byte[] valueBytes = this.description.getBytes(StandardCharsets.UTF_8);
		int valueLength = valueBytes.length;
		byte[] valueLengthBytes = Ints.toByteArray(valueLength);
		data = Bytes.concat(data, valueLengthBytes);
				
		//WRITE DESCRIPTION
		data = Bytes.concat(data, valueBytes);
		
		//WRITE OPTIONS SIZE
		byte[] optionsLengthBytes = Ints.toByteArray(this.options.size());
		data = Bytes.concat(data, optionsLengthBytes);
				
		//WRITE OPTIONS
		for(PollOption option: this.options)
		{
			data = Bytes.concat(data, option.toBytes());
		}
		
		return data;
	}
	
	public int getDataLength() 
	{
		int length = CREATOR_LENGTH + NAME_SIZE_LENGTH + this.name.getBytes(StandardCharsets.UTF_8).length + DESCRIPTION_SIZE_LENGTH + this.description.getBytes(StandardCharsets.UTF_8).length + OPTIONS_SIZE_LENGTH;
		
		for(PollOption option: this.options)
		{
			length += option.getDataLength();
		}
		
		return length;
	}

	public int addVoter(Account voter, int optionIndex) 
	{
		//CHECK IF WE HAD A PREVIOUS VOTE IN THIS POLL
		int previousOption = -1;
		for(PollOption option: this.options)
		{
			if(option.hasVoter(voter))
			{
				previousOption = this.options.indexOf(option);
			}
		}
		
		if(previousOption != -1)
		{
			//REMOVE VOTE
			this.options.get(previousOption).removeVoter(voter);
		}
		
		//ADD NEW VOTE
		this.options.get(optionIndex).addVoter(voter);
		
		return previousOption;
	}

	public void deleteVoter(Account voter, int optionIndex) 
	{
		this.options.get(optionIndex).removeVoter(voter);
	}
	
	//COPY
	
	public Poll copy()
	{
		try
		{
			byte[] bytes = this.toBytes();
			return parse(bytes);
		}
		catch(Exception e)
		{
			return null;
		}
	}
}
