/**
 * Copyright (c) 2015 Source Auditor Inc.
 *
 *   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.
 *
*/
package org.spdx.rdfparser.model;

import java.util.Calendar;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.spdx.rdfparser.IModelContainer;
import org.spdx.rdfparser.InvalidSPDXAnalysisException;
import org.spdx.rdfparser.RdfModelHelper;
import org.spdx.rdfparser.SPDXCreatorInformation;
import org.spdx.rdfparser.SPDXReview;
import org.spdx.rdfparser.SpdxDocumentContainer;
import org.spdx.rdfparser.SpdxRdfConstants;
import org.spdx.rdfparser.license.AnyLicenseInfo;
import org.spdx.rdfparser.license.ExtractedLicenseInfo;
import org.spdx.rdfparser.license.ListedLicenses;
import org.spdx.rdfparser.license.SpdxListedLicense;

import com.google.common.base.Objects;
import org.apache.jena.graph.Node;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Resource;

/**
 * An SpdxDocument is a summary of the contents, provenance, ownership and licensing
 * analysis of a specific software package.
 * This is, effectively, the top level of SPDX information.
 *
 * Documents always have a model
 *
 * @author Gary O'Neall
 *
 */
public class SpdxDocument extends SpdxElement {

	private SpdxDocumentContainer documentContainer;
	SPDXCreatorInformation creationInfo;	//TODO Refactor to RdfModelObject
	AnyLicenseInfo dataLicense;
	String specVersion;
	@Deprecated	// Replaced by annotations
	SPDXReview[] reviewers;

	/**
	 * @param documentContainer
	 * @param node
	 * @throws InvalidSPDXAnalysisException
	 */
	public SpdxDocument(SpdxDocumentContainer documentContainer, Node node)
			throws InvalidSPDXAnalysisException {
		super(documentContainer, node);
		this.documentContainer = documentContainer;
		getMyPropertiesFromModel();
		if (this.getCreationInfo() == null){
			String licenseListVersion = ListedLicenses.getListedLicenses().getLicenseListVersion();
			String creationDate = DateFormatUtils.format(Calendar.getInstance(), SpdxRdfConstants.SPDX_DATE_FORMAT);
			SPDXCreatorInformation creationInfo = new SPDXCreatorInformation(new String[] {  }, creationDate, null, licenseListVersion);
			setCreationInfo(creationInfo);
		}
		else if (StringUtils.isBlank(this.getCreationInfo().getLicenseListVersion())){
			this.getCreationInfo().setLicenseListVersion(ListedLicenses.getListedLicenses().getLicenseListVersion());
		}

	}
	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.model.RdfModelObject#getPropertiesFromModel()
	 */
	@Override
	public void getPropertiesFromModel() throws InvalidSPDXAnalysisException {
		super.getPropertiesFromModel();
		getMyPropertiesFromModel();
	}

	void getMyPropertiesFromModel() throws InvalidSPDXAnalysisException {
		dataLicense = findAnyLicenseInfoPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_DATA_LICENSE);
		creationInfo = findCreationInfoPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_CREATION_INFO);
		specVersion = findSinglePropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_VERSION);
		reviewers = findReviewPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_REVIEWED_BY);
	}


	/**
	 * @return all SPDX items connected directly to this document.  Does not include
	 * children SPDX items (e.g. files within packages).
	 * @throws InvalidSPDXAnalysisException
	 */
	public SpdxItem[] getDocumentDescribes() throws InvalidSPDXAnalysisException {
		Relationship[] allRelationships = this.getRelationships();
		int count = 0;
		for (int i = 0; i < allRelationships.length; i++) {
			if (allRelationships[i].getRelationshipType() == Relationship.RelationshipType.DESCRIBES &&
					allRelationships[i].getRelatedSpdxElement() instanceof SpdxItem) {
				count++;
			}
		}
		SpdxItem[] refresh = new SpdxItem[count];
		int refreshIndex = 0;
		for (int i = 0; i < allRelationships.length; i++) {
			if (allRelationships[i].getRelationshipType() == Relationship.RelationshipType.DESCRIBES &&
					allRelationships[i].getRelatedSpdxElement() instanceof SpdxItem) {
				refresh[refreshIndex++] = (SpdxItem)allRelationships[i].getRelatedSpdxElement();
			}
		}
		return refresh;
	}

	SpdxItem[] getPackagesFromItems(SpdxItem[] items) {
		int count = 0;
		for (int i = 0; i < items.length; i++) {
			if (items[i] instanceof SpdxPackage) {
				count++;
			}
		}
		SpdxItem[] retval = new SpdxItem[count];
		count = 0;
		for (int i = 0; i < items.length; i++) {
			if (items[i] instanceof SpdxPackage) {
				retval[count++] = items[i];
			}
		}
		return retval;
	}

	SpdxItem[] getFilesFromItems(SpdxItem[] items) {
		int count = 0;
		for (int i = 0; i < items.length; i++) {
			if (items[i] instanceof SpdxFile) {
				count++;
			}
		}
		SpdxItem[] retval = new SpdxItem[count];
		count = 0;
		for (int i = 0; i < items.length; i++) {
			if (items[i] instanceof SpdxFile) {
				retval[count++] = items[i];
			}
		}
		return retval;
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.model.RdfModelObject#getUri(org.spdx.rdfparser.IModelContainer)
	 */
	@Override
	public String getUri(IModelContainer modelContainer) throws InvalidSPDXAnalysisException {
		if (this.node != null && this.node.isURI()) {
			return this.node.getURI();
		} else {
			// for the document, the URI is the same as the namespace
			return modelContainer.getDocumentNamespace();
		}
	}

	/**
	 * @return The unique Document Namespace
	 * @throws InvalidSPDXAnalysisException
	 */
	public String getDocumentUri() throws InvalidSPDXAnalysisException {
		return this.getUri(documentContainer);
	}

	public String getDocumentNamespace() throws InvalidSPDXAnalysisException {
		String[] parts = this.getDocumentUri().split("#");
		return parts[0];
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.model.RdfModelObject#getType(org.apache.jena.rdf.model.Model)
	 */
	@Override
	public Resource getType(Model model) {
		return model.createResource(SpdxRdfConstants.SPDX_NAMESPACE + SpdxRdfConstants.CLASS_SPDX_DOCUMENT);
	}

	@Override
	public void populateModel() throws InvalidSPDXAnalysisException {
		super.populateModel();
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_DATA_LICENSE, this.dataLicense);
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_CREATION_INFO, this.creationInfo);
		setPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_EXTRACTED_LICENSES, this.documentContainer.getExtractedLicenseInfos());
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_VERSION, specVersion);
		setPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_REVIEWED_BY, reviewers);
	}

	/**
	 * @return the documentContainer
	 */
	public SpdxDocumentContainer getDocumentContainer() {
		return documentContainer;
	}

	/**
	 * @return the creationInfo
	 * @throws InvalidSPDXAnalysisException
	 */
	public SPDXCreatorInformation getCreationInfo() throws InvalidSPDXAnalysisException {
		if (this.resource != null && this.refreshOnGet) {
			try {
				//TODO Once CreationInfo has been refactored to an RdfModelObjet, check for equivalent
				creationInfo = findCreationInfoPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
						SpdxRdfConstants.PROP_SPDX_CREATION_INFO);
			} catch (InvalidSPDXAnalysisException e) {
				logger.error("Error getting creationInfo from model");
				throw(e);
			}
		}
		return creationInfo;
	}

	/**
	 * @param creationInfo the creationInfo to set
	 */
	public void setCreationInfo(SPDXCreatorInformation creationInfo) {
		this.creationInfo = creationInfo;
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_CREATION_INFO, this.creationInfo);
	}

	/**
	 * @return the dataLicense
	 * @throws InvalidSPDXAnalysisException
	 */
	public AnyLicenseInfo getDataLicense() throws InvalidSPDXAnalysisException {
		if (this.resource != null && this.refreshOnGet) {
			try {
				AnyLicenseInfo refresh = findAnyLicenseInfoPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
						SpdxRdfConstants.PROP_SPDX_DATA_LICENSE);
				if (refresh == null || !refresh.equals(this.dataLicense)) {
					this.dataLicense = refresh;
				}
			} catch (InvalidSPDXAnalysisException e) {
				logger.error("Error getting data license from model");
				throw(e);
			}
		}
		return this.dataLicense;
	}

	/**
	 * @param dataLicense the dataLicense to set
	 * @throws InvalidSPDXAnalysisException
	 */
	public void setDataLicense(AnyLicenseInfo dataLicense) throws InvalidSPDXAnalysisException {
		this.dataLicense = dataLicense;
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_DATA_LICENSE, this.dataLicense);
	}

	/**
	 * @return the externalDocumentRefs
	 * @throws InvalidSPDXAnalysisException
	 */
	public ExternalDocumentRef[] getExternalDocumentRefs() throws InvalidSPDXAnalysisException {
		return this.documentContainer.getExternalDocumentRefs();
	}

	/**
	 * @param externalDocumentRefs the externalDocumentRefs to set
	 * @throws InvalidSPDXAnalysisException
	 */
	public void setExternalDocumentRefs(ExternalDocumentRef[] externalDocumentRefs) throws InvalidSPDXAnalysisException {
		this.documentContainer.setExternalDocumentRefs(externalDocumentRefs);
	}

	/**
	 * @return the extractedLicenseInfos
	 * @throws InvalidSPDXAnalysisException
	 */
	public ExtractedLicenseInfo[] getExtractedLicenseInfos() throws InvalidSPDXAnalysisException {
		return this.documentContainer.getExtractedLicenseInfos();
	}


	/**
	 * @param extractedLicenseInfos the extractedLicenseInfos to set
	 * @throws InvalidSPDXAnalysisException
	 */
	public void setExtractedLicenseInfos(
			ExtractedLicenseInfo[] extractedLicenseInfos) throws InvalidSPDXAnalysisException {
		this.documentContainer.setExtractedLicenseInfos(extractedLicenseInfos);
	}

	/**
	 * @return the specVersion
	 */

	public String getSpecVersion() {
		if (this.resource != null && this.refreshOnGet) {
			specVersion = findSinglePropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
					SpdxRdfConstants.PROP_SPDX_VERSION);
		}
		return specVersion;
	}

	/**
	 * @return the reviewers
	 * @throws InvalidSPDXAnalysisException
	 */
	@Deprecated
	public SPDXReview[] getReviewers() throws InvalidSPDXAnalysisException {
		if (this.resource != null && this.refreshOnGet) {
			try {
				// Note - this will always create new objects which may be considered a bug
				// No intention to fix since the reviewers are deprecated as of 2.0
				reviewers = findReviewPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
						SpdxRdfConstants.PROP_SPDX_REVIEWED_BY);
			} catch (InvalidSPDXAnalysisException e) {
				logger.error("Error getting reviews from model");
				throw(e);
			}
		}
		return reviewers;
	}

	/**
	 * @param reviewers the reviewers to set
	 */
	@Deprecated
	public void setReviewers(SPDXReview[] reviewers) {
		this.reviewers = reviewers;
		setPropertyValues(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_REVIEWED_BY, reviewers);
	}

	/**
	 * @param specVersion the specVersion to set
	 */
	public void setSpecVersion(String specVersion) {
		this.specVersion = specVersion;
		setPropertyValue(SpdxRdfConstants.SPDX_NAMESPACE,
				SpdxRdfConstants.PROP_SPDX_VERSION, specVersion);
	}

	/* (non-Javadoc)
	 * @see org.spdx.rdfparser.model.IRdfModel#verify()
	 */
	@Override
	public List<String> verify() {
		List<String> retval = super.verify();
		// specVersion
		String docSpecVersion = "";	// note - this is used later in verify to verify version specific info
		if (this.specVersion == null || this.specVersion.isEmpty()) {
			retval.add("Missing required SPDX version");
			docSpecVersion = "UNKNOWN";
		} else {
			docSpecVersion = this.specVersion;
			String verify = this.documentContainer.verifySpdxVersion(docSpecVersion);
			if (verify != null) {
				retval.add(verify);
			}
		}
		// creationInfo
		try {
			SPDXCreatorInformation creator = this.getCreationInfo();
			if (creator == null) {
				retval.add("Missing required Creator");
			} else {
				List<String> creatorVerification = creator.verify();
				retval.addAll(creatorVerification);
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid creator information: "+e.getMessage());
		}
		// Reviewers
		try {
			SPDXReview[] reviews = this.getReviewers();
			if (reviews != null) {
				for (int i = 0; i < reviews.length; i++) {
					List<String> reviewerVerification = reviews[i].verify();
					retval.addAll(reviewerVerification);
				}
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid reviewers: "+e.getMessage());
		}
		// Extracted licensine infos
		try {
			ExtractedLicenseInfo[] extractedLicInfos = this.getExtractedLicenseInfos();
			if (extractedLicInfos != null) {
				for (int i = 0; i < extractedLicInfos.length; i++) {
					List<String> extractedLicInfoVerification = extractedLicInfos[i].verify();
					retval.addAll(extractedLicInfoVerification);
				}
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid extracted licensing info: "+e.getMessage());
		}
		// data license
		if (!docSpecVersion.equals(SpdxDocumentContainer.POINT_EIGHT_SPDX_VERSION) &&
				!docSpecVersion.equals(SpdxDocumentContainer.POINT_NINE_SPDX_VERSION)) { // added as a mandatory field in 1.0
			try {
				AnyLicenseInfo dataLicense = this.getDataLicense();
				if (dataLicense == null) {
					retval.add("Missing required data license");
				} else {
					if (!(dataLicense instanceof SpdxListedLicense)) {
						retval.add("Invalid license type for data license - must be an SPDX Listed license");
					} else {
						if (docSpecVersion.equals(SpdxDocumentContainer.ONE_DOT_ZERO_SPDX_VERSION))
							{
							if (!((SpdxListedLicense)dataLicense).getLicenseId().equals(
									SpdxDocumentContainer.SPDX_DATA_LICENSE_ID_VERSION_1_0)) {
								retval.add("Incorrect data license for SPDX version 1.0 document - found "+
										((SpdxListedLicense)dataLicense).getLicenseId()+", expected "+
										SpdxDocumentContainer.SPDX_DATA_LICENSE_ID_VERSION_1_0);
							}
						} else {
							if (!((SpdxListedLicense)dataLicense).getLicenseId().equals(
									SpdxDocumentContainer.SPDX_DATA_LICENSE_ID)) {
								retval.add("Incorrect data license for SPDX document - found "+
										((SpdxListedLicense)dataLicense).getLicenseId()+
									", expected "+SpdxDocumentContainer.SPDX_DATA_LICENSE_ID);
							}
						}
					}
				}
			} catch (InvalidSPDXAnalysisException e) {
				retval.add("Invalid data license: "+e.getMessage());
			}
		}
		// External document references
		try {
			ExternalDocumentRef[] externalRefs = this.getExternalDocumentRefs();
			for (int i = 0; i < externalRefs.length; i++) {
				retval.addAll(externalRefs[i].verify());
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid external document references: "+e.getMessage());
		}
		// documentDescribes relationships
		try {
			SpdxItem[] items = getDocumentDescribes();
			if (items.length == 0) {
				retval.add("Document must have at least one relationship of type DOCUMENT_DESCRIBES");
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid document items: "+e.getMessage());
		}
		try {
			List<SpdxElement> allElements = documentContainer.findAllElements();
			for (SpdxElement element:allElements) {
				if (!element.getId().equals(this.getId())) {
					retval.addAll(element.verify());
				}
			}
		} catch (InvalidSPDXAnalysisException e) {
			retval.add("Invalid elements: "+e.getMessage());
		}
		return retval;
	}

	@Override
	public boolean equivalent(IRdfModel o) {
		return this.equivalent(o, true);
	}

	@Override
	public boolean equivalent(IRdfModel o, boolean testRelationships) {
		if (o == this) {
			return true;
		}
		if (!(o instanceof SpdxDocument)) {
			return false;
		}
		if (!(super.equivalent(o, testRelationships))) {
			return false;
		}
		SpdxDocument comp = (SpdxDocument)o;
		try {
            return (Objects.equal(this.creationInfo, comp.getCreationInfo()) &&
                    Objects.equal(this.dataLicense, comp.getDataLicense()) &&
				arraysEquivalent(this.getExternalDocumentRefs(), comp.getExternalDocumentRefs(), testRelationships) &&
				RdfModelHelper.arraysEqual(this.getExtractedLicenseInfos(), comp.getExtractedLicenseInfos()) &&
                    RdfModelHelper.arraysEqual(this.reviewers, comp.getReviewers()) && Objects.equal(this.specVersion, comp.getSpecVersion()));
		} catch (InvalidSPDXAnalysisException ex) {
			logger.error("Error testing for equivalent",ex);
			return false;
		}
	}

	@Override
	protected String getNamePropertyName() {
		return SpdxRdfConstants.PROP_NAME;
	}
	//NOTE: We can  not implement clone since there is only one SPDX document per model


	/**
	 * This method has been replaced by getSpecVersion to match the specification property name
	 * @return
	 */
	@Deprecated
	public String getSpdxVersion() {
		return this.getSpecVersion();
	}


	/**
	 * This method has been replaced by getCreationInfo to match the specification property name
	 * @return
	 * @throws InvalidSPDXAnalysisException
	 */
	@Deprecated
	public SPDXCreatorInformation getCreatorInfo() throws InvalidSPDXAnalysisException {
		return this.getCreationInfo();
	}


	/**
	 * This method has been replaced by getComment to match the specification property name
	 * @return
	 */
	@Deprecated
	public String getDocumentComment() {
		return this.getComment();
	}
	/**
	 * This method has been replaced by getSpdxItems
	 * This method will fail unless there is one and only 1 SPDX document
	 * @return
	 * @throws InvalidSPDXAnalysisException
	 */
	@Deprecated
	public SpdxPackage getSpdxPackage() throws InvalidSPDXAnalysisException {
		SpdxItem[] retval = this.getDocumentDescribes();
		if (retval.length != 1) {
			throw(new InvalidSPDXAnalysisException("More than one SPDX package defined in the document.  Must use getSpdxItems - Likely this application has not been upgraded for SPDX 2.0"));
		}
		return (SpdxPackage)retval[0];
	}
	/**
	 * @param license license to be added to the extracted licensing infos
	 * @throws InvalidSPDXAnalysisException
	 */
	public void addExtractedLicenseInfos(
			ExtractedLicenseInfo license) throws InvalidSPDXAnalysisException {
		this.documentContainer.addExtractedLicenseInfos( license );
	}
}
