/***********************************************************
Copyright (C) 2018 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-0107  USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
 ***********************************************************/
package com.verisign.epp.codec.validate.v02;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.verisign.epp.codec.gen.EPPCodecComponent;
import com.verisign.epp.codec.gen.EPPCodecException;
import com.verisign.epp.codec.gen.EPPDecodeException;
import com.verisign.epp.codec.gen.EPPEncodeException;
import com.verisign.epp.codec.gen.EPPUtil;
import com.verisign.epp.util.EPPCatFactory;
import com.verisign.epp.util.EqualityUtil;

/**
 * Represents a contact address. Every contact has associated postal address
 * information. A postal address contains OPTIONAL street information, city
 * information, OPTIONAL state/province information, an OPTIONAL postal code,
 * and a country identifier as described in [ISO11180]. Address information MAY
 * be provided in both a subset of UTF-8 [RFC2279] that can be represented in
 * 7-bit ASCII [US-ASCII] and unrestricted UTF-8. A contact address is defined
 * as the following:<br>
 * <br>
 * A &lt;validate:addr&gt; element that contains address information associated
 * with the contact. A &lt;validate:addr&gt; element contains the following
 * child elements:<br>
 * <br>
 *
 * <ul>
 * <li>OPTIONAL &lt;validate:street&gt; elements (up to a maximum of three) that
 * contain the contact's street address. Use {@link #getStreets()} and
 * {@link #setStreets(List)} to get and set the elements.</li>
 * <li>A &lt;validate:city&gt; element that contains the contact's city. Use
 * {@link #getCity()} and {@link #setCity(String)} to get and set the element.
 * </li>
 * <li>A &lt;validate:sp&gt; element that contains the contact's state or
 * province. This element is OPTIONAL for addressing schemes that do not require
 * a state or province name. Use {@link #getStateProvince()} and
 * {@link #setStateProvince(String)} to get and set the element.</li>
 * <li>An OPTIONAL &lt;validate:pc&gt; element that contains the contact's
 * postal code. Use {@link #getPostalCode()} and {@link #setPostalCode(String)}
 * to get and set the element.</li>
 * <li>A &lt;validate:cc&gt; element that contains the two-character identifier
 * representing with the contact's country. Use {@link #getCountry()} and
 * {@link #setCountry(String)} to get and set the element.</li>
 * </ul>
 */
public class EPPValidateAddress implements EPPCodecComponent {

	/**
	 * Log4j category for logging
	 */
	private static Logger cat = Logger.getLogger(EPPValidateAddress.class.getName(),
	      EPPCatFactory.getInstance().getFactory());

	/**
	 * XML local name for {@code EPPValidateAddress}.
	 */
	public static final String ELM_LOCALNAME = "addr";

	/**
	 * XML root tag for {@code EPPValidatePostalDefinition}.
	 */
	public static final String ELM_NAME = EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_LOCALNAME;

	/**
	 * XML tag name for an streets attribute.
	 */
	private final static String ELM_STREET = "street";

	/**
	 * XML tag name for an city attribute.
	 */
	private final static String ELM_CITY = "city";

	/**
	 * XML tag name for an stateProvince attribute.
	 */
	private final static String ELM_STATE_PROVINCE = "sp";

	/**
	 * XML tag name for an postalCode attribute.
	 */
	private final static String ELM_POSTAL_CODE = "pc";

	/**
	 * XML tag name for an country attribute.
	 */
	private final static String ELM_COUNTRY = "cc";

	/**
	 * XML tag name for an streets attribute.
	 */
	private final static int MAX_STREET = 3;

	/**
	 * Contact street, which is a {@code List} of up to 3 {@code String}'s
	 * representing street line 1, line 2, and line 3.
	 */
	private List<String> streets = new ArrayList<String>();

	/**
	 * Contact city.
	 */
	private String city;

	/**
	 * Contact state/province.
	 */
	private String stateProvince;

	/**
	 * Contact postal code
	 */
	private String postalCode;

	/**
	 * Contact country
	 */
	private String country;

	/**
	 * Default constructor for {@code EPPValidateAddress}. All the the attributes
	 * default to {@code null}. Must call required setter methods before invoking
	 * {@link #encode(Document)}, which include:<br>
	 * <br>
	 *
	 * <ul>
	 * <li>City - {@link #setCity(String)}</li>
	 * <li>Country - {@link #setCountry(String)}</li>
	 * </ul>
	 */
	public EPPValidateAddress() {
	}

	/**
	 * Constructor for {@code EPPValidateAddress} all of the required attributes
	 * as parameters.
	 *
	 * @param aCity
	 *           Contact street
	 * @param aCountry
	 *           Contact country
	 */
	public EPPValidateAddress(String aCity, String aCountry) {
		this.city = aCity;
		this.country = aCountry;
	}

	/**
	 * Constructor for {@code EPPValidateAddress} all of the attributes as
	 * parameters.
	 *
	 * @param aStreets
	 *           {@code List&lt;String&gt;} collection of streets (up to maximum
	 *           three)
	 * @param aCity
	 *           Contact street
	 * @param aStateProvince
	 *           Contact state/province
	 * @param aPostalCode
	 *           Contact postal code
	 * @param aCountry
	 *           Contact country
	 */
	public EPPValidateAddress(List<String> aStreets, String aCity, String aStateProvince, String aPostalCode,
	      String aCountry) {
		this.streets = aStreets;
		this.city = aCity;
		this.stateProvince = aStateProvince;
		this.postalCode = aPostalCode;
		this.country = aCountry;
	}

	/**
	 * Is there any street lines set?
	 *
	 * @return {@code true} if there is at least one street line set.
	 */
	public boolean hasStreets() {
		if (this.streets != null && !this.streets.isEmpty()) {
			return true;
		}
		else {
			return false;
		}
	}

	/**
	 * Add a street line to the street. This will add {@code aStreetLine} to the
	 * list of street lines.
	 *
	 * @param aStreetLine
	 *           Street line to add to the street
	 */
	public void addStreet(String aStreetLine) {
		this.streets.add(aStreetLine);
	}

	/**
	 * Gets the contact street(s).
	 *
	 * @return street(s) as a {@code List&lt;String&gt;} of streets (up to
	 *         maximum three).
	 */
	public List<String> getStreets() {
		return this.streets;
	}

	/**
	 * Sets the contact streets attribute with a {@code List&gt;String&gt;},
	 * where each element represents a line of the street address.
	 *
	 * @param aStreets
	 *           Up to 3 street elements
	 */
	public void setStreets(List<String> aStreets) {
		this.streets = aStreets;
	}

	/**
	 * Sets the contact streets with only one line. Only a one element
	 * {@code List&lt;String&gt;} will be returned on a call to
	 * {@link #getStreets()} after calling this method.
	 *
	 * @param aStreet
	 *           Contact street.
	 */
	public void setStreet(String aStreet) {
		this.streets = new ArrayList<String>();
		this.streets.add(aStreet);
	}

	/**
	 * Sets the contact street with two street lines.
	 *
	 * @param aStreet1
	 *           First street line
	 * @param aStreet2
	 *           Second street line
	 */
	public void setStreets(String aStreet1, String aStreet2) {
		this.streets = new ArrayList<String>();

		this.streets.add(aStreet1);
		this.streets.add(aStreet2);
	}

	/**
	 * Sets the contact street with three street lines.
	 *
	 * @param aStreet1
	 *           First street line
	 * @param aStreet2
	 *           Second street line
	 * @param aStreet3
	 *           Third street line
	 */
	public void setStreets(String aStreet1, String aStreet2, String aStreet3) {
		this.streets = new ArrayList<String>();

		this.streets.add(aStreet1);
		this.streets.add(aStreet2);
		this.streets.add(aStreet3);
	}

	/**
	 * Gets the contact city.
	 *
	 * @return city. {@code String} if defined; {@code null} otherwise.
	 */
	public String getCity() {
		return this.city;
	}

	/**
	 * Sets the contact city.
	 *
	 * @param aCity
	 *           contact city
	 */
	public void setCity(String aCity) {
		this.city = aCity;
	}

	/**
	 * Gets the contact state/province.
	 *
	 * @return state/province. {@code String} if defined; {@code null} otherwise.
	 */
	public String getStateProvince() {
		return this.stateProvince;
	}

	/**
	 * Sets the contact state/province.
	 *
	 * @param aStateProvince
	 *           contact state/province
	 */
	public void setStateProvince(String aStateProvince) {
		this.stateProvince = aStateProvince;
	}

	/**
	 * Gets the contact postal code
	 *
	 * @return postal code {@code String} if defined; {@code null} otherwise.
	 */
	public String getPostalCode() {
		return this.postalCode;
	}

	/**
	 * Sets the contact postal code
	 *
	 * @param aPostalCode
	 *           contact postal code
	 */
	public void setPostalCode(String aPostalCode) {
		this.postalCode = aPostalCode;
	}

	/**
	 * Gets the contact country.
	 *
	 * @return contact country {@code String} if defined; {@code null} otherwise.
	 */
	public String getCountry() {
		return this.country;
	}

	/**
	 * Sets the contact country.
	 *
	 * @param aCountry
	 *           contact country
	 */
	public void setCountry(String aCountry) {
		this.country = aCountry;
	}

	/**
	 * Validate the state of the {@code EPPValidateAddress} instance. A valid
	 * state means that all of the required attributes have been set. If
	 * validateState returns without an exception, the state is valid. If the
	 * state is not valid, the EPPCodecException will contain a description of
	 * the error. throws EPPCodecException State error. This will contain the
	 * name of the attribute that is not valid.
	 *
	 * @throws EPPCodecException
	 *            On validation error
	 */
	void validateState() throws EPPCodecException {

		if (this.streets != null) {
			if (this.streets.contains(null)) {
				throw new EPPCodecException("street lines cannot be set to null");
			}
		}

		if ((this.streets != null) && !this.streets.isEmpty() && (this.streets.size() > MAX_STREET)) {
			throw new EPPCodecException("street lines exceed the maximum");
		}

		if (this.city == null) {
			throw new EPPCodecException("city required attribute is not set");
		}

		if (this.country == null) {
			throw new EPPCodecException("country required attribute is not set");
		}
	}

	/**
	 * Encode a DOM Element tree from the attributes of the
	 * {@code EPPValidateAddress} instance.
	 *
	 * @param aDocument
	 *           DOM Document that is being built. Used as an Element factory.
	 *
	 * @return Root DOM Element representing the {@code EPPValidateAddress}
	 *         instance.
	 *
	 * @exception EPPEncodeException
	 *               Unable to encode {@code EPPValidateAddress} instance.
	 */
	@Override
	public Element encode(Document aDocument) throws EPPEncodeException {
		try {
			validateState();
		}
		catch (EPPCodecException e) {
			throw new EPPEncodeException("Invalid state on EPPValidateAddress.encode: " + e);
		}

		// Create root element
		Element root = aDocument.createElementNS(EPPValidateMapFactory.NS_CONTACT, ELM_NAME);

		// Street
		EPPUtil.encodeStringList(aDocument, root, this.streets, EPPValidateMapFactory.NS_CONTACT,
		      EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_STREET);

		// City
		EPPUtil.encodeString(aDocument, root, this.city, EPPValidateMapFactory.NS_CONTACT,
		      EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_CITY);

		// State/Province
		EPPUtil.encodeString(aDocument, root, this.stateProvince, EPPValidateMapFactory.NS_CONTACT,
		      EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_STATE_PROVINCE);

		// Postal Code
		EPPUtil.encodeString(aDocument, root, this.postalCode, EPPValidateMapFactory.NS_CONTACT,
		      EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_POSTAL_CODE);

		// Country
		EPPUtil.encodeString(aDocument, root, this.country, EPPValidateMapFactory.NS_CONTACT,
		      EPPValidateMapFactory.NS_PREFIX_CONTACT + ":" + ELM_COUNTRY);

		return root;
	}

	/**
	 * Decode the {@code EPPValidateAddress} attributes from the aElement DOM
	 * Element tree.
	 *
	 * @param aElement
	 *           Root DOM Element to decode {@code EPPValidateAddress} from.
	 *
	 * @exception EPPDecodeException
	 *               Unable to decode aElement.
	 */
	@Override
	public void decode(Element aElement) throws EPPDecodeException {

		// Street
		this.streets = EPPUtil.decodeList(aElement, EPPValidateMapFactory.NS_CONTACT, ELM_STREET);

		// City
		this.city = EPPUtil.decodeString(aElement, EPPValidateMapFactory.NS_CONTACT, ELM_CITY);

		// State/Province
		this.stateProvince = EPPUtil.decodeString(aElement, EPPValidateMapFactory.NS_CONTACT, ELM_STATE_PROVINCE);

		// Postal Code
		this.postalCode = EPPUtil.decodeString(aElement, EPPValidateMapFactory.NS_CONTACT, ELM_POSTAL_CODE);

		// Country
		this.country = EPPUtil.decodeString(aElement, EPPValidateMapFactory.NS_CONTACT, ELM_COUNTRY);
	}

	/**
	 * implements a deep {@code EPPValidateAddress} compare.
	 *
	 * @param aObject
	 *           {@code EPPValidateAddress} instance to compare with
	 *
	 * @return {@code true} of {@code aObject} is equal to instance;
	 *         {@code false} otherwise.
	 */
	@Override
	public boolean equals(Object aObject) {

		if (!(aObject instanceof EPPValidateAddress)) {
			return false;
		}

		EPPValidateAddress other = (EPPValidateAddress) aObject;

		// Street
		if (!EPPUtil.equalLists(this.streets, other.streets)) {
			cat.error("EPPValidateAddress.equals(): streets not equal");
			return false;
		}

		// City
		if (!EqualityUtil.equals(this.city, other.city)) {
			cat.error("EPPValidateAddress.equals(): city not equal");
			return false;
		}

		// State/Province
		if (!EqualityUtil.equals(this.stateProvince, other.stateProvince)) {
			cat.error("EPPValidateAddress.equals(): stateProvince not equal");
			return false;
		}

		// Postal Code
		if (!EqualityUtil.equals(this.postalCode, other.postalCode)) {
			cat.error("EPPValidateAddress.equals(): postalCode not equal");
			return false;
		}

		// Country
		if (!EqualityUtil.equals(this.country, other.country)) {
			cat.error("EPPValidateAddress.equals(): country not equal");
			return false;
		}

		return true;
	}

	/**
	 * Clone {@code EPPValidateAddress}.
	 *
	 * @return clone of {@code EPPValidateAddress}
	 *
	 * @exception CloneNotSupportedException
	 *               standard Object.clone exception
	 */
	@Override
	public Object clone() throws CloneNotSupportedException {
		EPPValidateAddress clone = null;

		clone = (EPPValidateAddress) super.clone();

		if (this.hasStreets()) {
			clone.streets = (List) ((ArrayList) this.streets).clone();
		}

		return clone;
	}

	/**
	 * Implementation of {@code Object.toString}, which will result in an
	 * indented XML {@code String} representation of the concrete
	 * {@code EPPCodecComponent}.
	 *
	 * @return Indented XML {@code String} if successful; {@code ERROR}
	 *         otherwise.
	 */
	@Override
	public String toString() {
		return EPPUtil.toString(this);
	}

	/**
	 * Returns the XML namespace associated with the {@code EPPCodecComponent}.
	 *
	 * @return XML namespace for the {@code EPPCodecComponent}.
	 */
	@Override
	public String getNamespace() {
		return EPPValidateMapFactory.NS;
	}

}
