package org.matthiaszimmermann.ethereum.pwg;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;

import org.web3j.crypto.Credentials;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.crypto.Wallet;
import org.web3j.crypto.WalletFile;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.ObjectMapperFactory;
import org.web3j.protocol.core.methods.request.RawTransaction;
import org.web3j.utils.Numeric;

import com.fasterxml.jackson.databind.ObjectMapper;

public class PaperWallet {

	public static final int PHRASE_SIZE_DEFAULT = 8;
	public static final String WALLET_OK = "OK";
	public static final String WALLET_ERROR = "ERROR";
	
	// 20 GWei as of august '17. check http://ethgasstation.info/ or similar
	public static BigInteger GAS_PRICE_DEFAULT = BigInteger.valueOf(20_000_000_000L);
	
	// 21'000 gas. check https://ethereum.stackexchange.com/questions/5845/how-are-ethereum-transaction-costs-calculated
	public static BigInteger GAS_LIMIT_DEFAULT = BigInteger.valueOf(21_000L);

	private static PassPhraseUtility passPhraseUtility = new PassPhraseUtility();

	private Credentials credentials = null;
	private String fileName;
	private String pathToFile;
	private String passPhrase;
	
	public PaperWallet(String passPhrase, File walletFile) {
		// check if provided file exists
		if(!walletFile.exists() || walletFile.isDirectory()) { 
			System.err.println(String.format("%s file does not exist or is a directory", WALLET_ERROR));
		}
		
		try {
			credentials = WalletUtils.loadCredentials(passPhrase, walletFile);
		} 
		catch (Exception e) {
			System.err.println(String.format("%s failed to load credentials with provided password", WALLET_ERROR));
		}
	}

	public PaperWallet(String passPhrase, String pathToFile) throws Exception {
		this.passPhrase = setPassPhrase(passPhrase);
		this.pathToFile = setPathToFile(pathToFile);

		try {
			fileName = WalletUtils.generateNewWalletFile(this.passPhrase, new File(this.pathToFile));
			credentials = getCredentials(this.passPhrase);
		}
		catch (Exception e) {
			throw new Exception("Failed to create account", e);
		}
	}
	
	public String createOfflineTx(String toAddress, BigInteger gasPrice, BigInteger gasLimit, BigInteger amount, BigInteger nonce) {
		RawTransaction rawTransaction  = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, toAddress, amount);
		byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
		String hexValue = Numeric.toHexString(signedMessage);
		
		return hexValue;
	}

	public static String getPathToFileDefault() {
		return WalletUtils.getDefaultKeyDirectory();
	}
	
	public static String checkWalletFileStatus(File sourceFile, String passPhrase) {
		// check if provided file exists
		if(!sourceFile.exists() || sourceFile.isDirectory()) { 
			return String.format("%s file does not exist or is a directory", WALLET_ERROR);
		}
		
		WalletFile walletFile = null;

		// try to create wallet file object
		try {
	        ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
	        walletFile = objectMapper.readValue(sourceFile, WalletFile.class);
		} 
		catch (Exception e) {
			String message = e.getLocalizedMessage();
			
			if(message == null) {
				message = "general wallet file format error";
			}
			
			return String.format("%s %s", WALLET_ERROR, message);
		}
		
		// try to decrypt wallet file object
		ECKeyPair keyPair = null;
		
		try {
	        keyPair = Wallet.decrypt(passPhrase, walletFile);
		} 
		catch (Exception e) {
			String message = e.getLocalizedMessage();
			
			if(message == null) {
				message = "general wallet file decryption error";
			}
			
			return String.format("%s %s", WALLET_ERROR, message);
		}
		
		if(keyPair != null) {
			String privateK = encode(keyPair.getPrivateKey());
			String publicK = encode(keyPair.getPublicKey());
			return String.format("%s\npublic key: %s\nprivate key: %s", WALLET_OK, publicK, privateK);
		}
		else {
			return WALLET_OK;
		}
	}

	public Credentials getCredentials(String passPhrase) throws Exception {
		if (credentials != null) {
			return credentials;
		}

		try {
			String fileWithPath = getFile().getAbsolutePath();
			credentials = WalletUtils.loadCredentials(passPhrase, fileWithPath);

			return credentials;
		}
		catch (Exception e) {
			throw new Exception ("Failed to access credentials in file '" + getFile().getAbsolutePath() + "'", e);
		}
	}

	public String getAddress() {
		return credentials.getAddress();
	}

	public String getPassPhrase() {
		return passPhrase;
	}

	public String getPathToFile() {
		return pathToFile;
	}

	public String getFileName() {
		return fileName;
	}

	public String getFileContent() throws Exception {
		try {
			return String.join("", Files.readAllLines(getFile().toPath()));
		} 
		catch (IOException e) {
			throw new Exception ("Failed to read content from file '" + getFile().getAbsolutePath() + "'", e);
		}
	}

	public File getFile() {
		return new File(pathToFile, fileName);
	}

	private String setPassPhrase(String passPhrase) {
		if(passPhrase == null || passPhrase.isEmpty()) {
			return passPhraseUtility.getPassPhrase(PHRASE_SIZE_DEFAULT);
		}

		return passPhrase;
	}

	private String setPathToFile(String pathToFile) {
		if(pathToFile == null || pathToFile.isEmpty()) {
			return getPathToFileDefault();
		}

		return pathToFile;
	}

	public String getBaseName() {
		if(fileName == null) {
			return null;
		}
		
		int pos = fileName.lastIndexOf(".");
		
		if(pos >= 0) {
			return fileName.substring(0, pos);
		}
		
		return fileName;
	}
	
	private static String encode(BigInteger number) {
		// TODO check that this is the desired encoding...
		return Keys.getAddress(number);
	}

}