BrAPIv2Mapper.java

/*
 * Copyright 2023 Global Crop Diversity Trust
 *
 * 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.gringlobal.brapi.v2;

import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.filters.StringFilter;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.CropTrait;
import org.gringlobal.model.CropTraitCode;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.CommunityCodeValues.CodeValueDef;
import org.gringlobal.service.CropTraitCodeTranslationService.TranslatedCropTraitCode;
import org.gringlobal.service.CropTraitTranslationService.TranslatedCropTrait;
import org.gringlobal.service.SiteService;
import org.gringlobal.service.TaxonomySpeciesCRUDService;
import org.gringlobal.service.filter.SiteFilter;
import org.gringlobal.worker.AccessionMCPDConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Category;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Method;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Scale;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Scale.DataType;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.Trait;
import uk.ac.hutton.ics.brapi.resource.germplasm.attribute.ValidValues;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Collection;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Collsite;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Germplasm;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Institute;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.Mcpd;
import uk.ac.hutton.ics.brapi.resource.germplasm.germplasm.McpdDonor;
import uk.ac.hutton.ics.brapi.resource.phenotyping.observation.ObservationVariable;

@Component
public class BrAPIv2Mapper {

	@Autowired
	@Lazy
	private AccessionMCPDConverter accessionMcpdConverter;

	@Autowired
	@Lazy
	private SiteService siteService;

	@Autowired
	@Lazy
	private TaxonomySpeciesCRUDService taxonomySpeciesService;

	public static <A, B> List<B> map(List<A> source, Function<A, B> mapper) {
		return source.stream().map(mapper).collect(Collectors.toList());
	}

	public static <A, B> Page<B> map(Page<A> source, Function<A, B> mapper) {
		return source.map(mapper);
	}

	/**
	 * Allows mapping of {@code null} objects to {@code null}.
	 *
	 * @param <SOURCE> source type
	 * @param <TARGET> target type
	 * @param source   Source object, may be null
	 * @param mapper   mapping function from {@code SOURCE} to {@code TARGET} type
	 * @return
	 */
	public static <SOURCE, TARGET> TARGET mapNullable(SOURCE source, Function<SOURCE, TARGET> mapper) {
		if (source == null)
			return null;
		return mapper.apply(source);
	}

	private String map(Long value) {
		return value == null ? null : Long.toString(value);
	}

	private static ThreadLocal<NumberFormat> df = new ThreadLocal<>() {
		protected NumberFormat initialValue() {
			return NumberFormat.getNumberInstance(Locale.ROOT);
		};
	};

	private String map(Double value) {
		return value == null ? null : df.get().format(value);
	}

	public Germplasm map(Accession a) {
		var dto = new Germplasm();
		dto.setGermplasmDbId(Long.toString(a.getId()));
		dto.setAccessionNumber(a.getAccessionNumber());
		if (a.getInitialReceivedDate() != null)
			dto.setAcquisitionDate(Instant.ofEpochMilli(a.getInitialReceivedDate().getTime()).atOffset(ZoneOffset.UTC)
				.format(DateTimeFormatter.ISO_LOCAL_DATE));
		dto.setDefaultDisplayName(a.getAccessionNumber());
		dto.setGenus(a.getTaxonomySpecies().getTaxonomyGenus().getName());
		dto.setSpecies(a.getTaxonomySpecies().getSpecificEpithet());
		dto.setSpeciesAuthority(a.getTaxonomySpecies().getSpeciesAuthority());
		dto.setGermplasmName(a.getPreferredName());
		dto.setGermplasmPUI(a.getDoi());
		dto.setSubtaxa(a.getTaxonomySpecies().getSubTaxon());
		if (dto.getSubtaxa() != null) dto.setSubtaxaAuthority(a.getTaxonomySpecies().getNameAuthority());
		dto.setInstituteCode(a.getSite().getFaoInstituteNumber());
		
//		dto.setSynonyms(a.getNames().stream()
//			// filter
//			.filter(name -> Objects.equals("Y", name.getIsWebVisible()))
//			// map to Synonym
//			.map(name -> new Synonym().setSynonym(name.getPlantName()).setType(name.getCategoryCode()))
//			// collect
//			.collect(Collectors.toList())
//		);
		return dto;
	}

	public Accession map(Germplasm a) {
		var dto = new Accession();
		if (a.getGermplasmDbId() != null) dto.setId(Long.parseLong(a.getGermplasmDbId()));
		dto.setAccessionNumber(a.getAccessionNumber());
		dto.setAccessionNumberPart1(a.getAccessionNumber()); // Can we split this?
		if (a.getAcquisitionDate() != null) {
			var receivedDate = LocalDate.parse(a.getAcquisitionDate(), DateTimeFormatter.ISO_LOCAL_DATE);
			dto.setInitialReceivedDate(Date.from(receivedDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant()));
			dto.setInitialReceivedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
		}
//		dto.setGenus(a.getTaxonomySpecies().getTaxonomyGenus().getName());
//		dto.setSpecies(a.getTaxonomySpecies().getSpecificEpithet());
//		dto.setSpeciesAuthority(a.getTaxonomySpecies().getSpeciesAuthority());
//		dto.setSubtaxa(a.getTaxonomySpecies().getSubTaxon());
//		dto.setSubtaxaAuthority(a.getTaxonomySpecies().getSubTaxon())
//		dto.setPreferredName(a.getGermplasmName());
		dto.setDoi(a.getGermplasmPUI());

		// Site
		var siteFilter = new SiteFilter()
			.faoInstituteNumber(new StringFilter().eq(Set.of(a.getInstituteCode())));
		try {
		var sites = siteService.list(siteFilter, Pageable.ofSize(1));
		if (sites.hasContent()) dto.setSite(sites.getContent().get(0));
		} catch (SearchException e) {
		}

		var taxonomySpecies = taxonomySpeciesService.fromMCPD(a.getGenus(), a.getSpecies(), a.getSpeciesAuthority(), a.getSubtaxa(), a.getSubtaxaAuthority());
		dto.setTaxonomySpecies(taxonomySpecies);

		return dto;
	}


	public Mcpd mapMcpd(Accession a) {
		var mcpd = accessionMcpdConverter.convert(a);
		var dto = new Mcpd();
		dto.setGermplasmDbId(Long.toString(a.getId()));
		dto.setAccessionNumber(mcpd.acceNumb);
		dto.setGermplasmPUI(mcpd.puid);
		dto.setInstituteCode(mcpd.instCode);
		dto.setAccessionNumber(mcpd.acceNumb);
		dto.setAcquisitionDate(mcpd.acqDate);

		dto.setGenus(mcpd.genus);
		dto.setSpecies(mcpd.species);
		dto.setSpeciesAuthority(mcpd.spAuthor);
		dto.setSubtaxon(mcpd.subtaxa);
		dto.setSubtaxonAuthority(mcpd.subtAuthor);

		if (mcpd.collSrc != null) dto.setAcquisitionSourceCode(mcpd.collSrc.toString());
		dto.setAncestralData(mcpd.ancest);
		if (mcpd.sampStat != null) dto.setBiologicalStatusOfAccessionCode(mcpd.sampStat.toString());
		
		dto.setBreedingInstitutes(toBrapiInstitutes(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_DEVELOPED));
		dto.setCollectingInfo(new Collection()
			.setCollectingDate(mcpd.collDate)
			.setCollectingInstitutes(toBrapiInstitutes(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_COLLECTED))
			.setCollectingMissionIdentifier(mcpd.collMissid)
			.setCollectingNumber(mcpd.collNumb)
			.setCollectingSite(new Collsite()
//				.setCoordinateUncertainty(mcpd.coordUncert) // special formatting
//				.setElevation(mcpd.elevation) // special formatting
				.setGeoreferencingMethod(mcpd.geoRefMeth)
//				.setLatitudeDecimal(mcpd.decLatitude) // special formatting
				.setLatitudeDegrees(null)
//				.setLongitudeDecimal(mcpd.decLongitude) // special formatting
				.setLongitudeDegrees(null)
				.setSpatialReferenceSystem(mcpd.coordDatum)
			)
		);

		if (mcpd.decLatitude != null) dto.getCollectingInfo().getCollectingSite().setLatitudeDecimal(df.get().format(mcpd.decLatitude));
		if (mcpd.decLongitude != null) dto.getCollectingInfo().getCollectingSite().setLongitudeDecimal(df.get().format(mcpd.decLongitude));
		if (mcpd.coordUncert != null) dto.getCollectingInfo().getCollectingSite().setCoordinateUncertainty(df.get().format(mcpd.coordUncert));
		if (mcpd.elevation != null) dto.getCollectingInfo().getCollectingSite().setElevation(df.get().format(mcpd.elevation));

		dto.setCommonCropName(mcpd.cropName);
		dto.setCountryOfOrigin(mcpd.origCty);
		dto.setDonorInfo(new McpdDonor()
			.setDonorAccessionNumber(mcpd.donorNumb)
//			.setDonorAccessionPui(null)
			.setDonorInstitute(toBrapiInstitute(a.getAccessionSources(), CommunityCodeValues.ACCESSION_SOURCE_TYPE_DONATED))
		);
		if (mcpd.mlsStat != null) dto.setMlsStatus(mcpd.mlsStat.toString());
		dto.setRemarks(mcpd.remarks);
		dto.setSafetyDuplicateInstitutes(
			Arrays.asList(a.getBackupLocation1Site(), a.getBackupLocation2Site()).stream()
			// filter
			.filter(s -> s != null)
			// map
			.map(site -> new Institute()
				.setInstituteCode(site.getFaoInstituteNumber())
				.setInstituteName(site.getSiteLongName())
//				.setInstituteAddress()
			)
			.collect(Collectors.toList())
		);
		if (StringUtils.isNotBlank(mcpd.storage)) dto.setStorageTypeCodes(Arrays.asList(mcpd.storage.split(";")));

		return dto;
	}

	private static Institute toBrapiInstitute(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
		var institutes = toBrapiInstitutes(accessionSources, sourceType);
		return CollectionUtils.isEmpty(institutes) ? null : institutes.get(0);
	}

	private static List<Institute> toBrapiInstitutes(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
		if (CollectionUtils.isEmpty(accessionSources)) return null;
		return filterSource(accessionSources, sourceType)
			// map
			.map(AccessionSource::getCooperators)
			// reduce
			.flatMap(List::stream)
			.map(coop -> new Institute()
				.setInstituteName(coop.getOrganization())
				.setInstituteCode(coop.getFaoInstituteNumber())
				.setInstituteAddress(StringUtils.joinWith("\n", coop.getAddressLine1(), coop.getAddressLine2(), coop.getAddressLine3()))
			)
			.distinct()
			.collect(Collectors.toList());
	}

	private static Stream<AccessionSource> filterSource(List<AccessionSource> accessionSources, CodeValueDef sourceType) {
		if (CollectionUtils.isEmpty(accessionSources)) return Stream.ofNullable(null);
		return accessionSources.stream()
			// filter
			.filter(src -> Objects.equals(src.getSourceTypeCode(), sourceType.value));
	}

	public CropTrait map(Trait trait) {
		var dto = new CropTrait();
		if (trait.getTraitDbId() != null) dto.setId(Long.parseLong(trait.getTraitDbId()));
		dto.setCodedName(trait.getMainAbbreviation());
		dto.setCategoryCode(trait.getTraitClass());
		return dto;
	}

	public Trait map(CropTrait trait) {
		if (trait == null) return null;
		var dto = new Trait();
		dto.setTraitDbId(map(trait.getId()));
		dto.setTraitName(trait.getCodedName());
//		dto.setStatus(trait.getIsPeerReviewed());
		dto.setMainAbbreviation(trait.getCodedName());
		dto.setTraitClass(trait.getCategoryCode());
		dto.setSynonyms(List.of());
		return dto;
	}

	public Trait map(TranslatedCropTrait trait) {
		if (trait == null) return null;
		var dto = map(trait.entity);
		dto.setTraitName(trait.title);
		dto.setTraitDescription(trait.description);
		return dto;
	}

	public ObservationVariable mapVariable(CropTrait trait) {
		var dto = new ObservationVariable();
		dto.setObservationVariableDbId(map(trait.getId()));
		dto.setObservationVariableName(trait.getCodedName());
		dto.setTrait(map(trait));
		dto.setMethod(mapMethod(trait));
		dto.setScale(mapScale(trait));
		dto.setCommonCropName(trait.getCrop().getName());
//		dto.setContextOfUse(null)
//		dto.setAdditionalInfo(null)
		dto.setSynonyms(List.of()); // NPE in FieldBook
		return dto;
	}

	public ObservationVariable mapVariable(TranslatedCropTrait trait) {
		var dto = mapVariable(trait.entity);
		dto.setTrait(map(trait));
		dto.setMethod(mapMethod(trait));
		dto.setScale(mapScale(trait));
		return dto;
	}

	
	private Method mapMethod(TranslatedCropTrait trait) {
		if (trait == null) return null;
		var dto = mapMethod(trait.entity);
		dto.setMethodName(trait.title);
		dto.setDescription(trait.description);
		return dto;
	}

	private Method mapMethod(CropTrait trait) {
		var dto = new Method();
		dto.setMethodDbId(map(trait.getId()));
		dto.setMethodName(trait.getCodedName());
		dto.setMethodClass(trait.getCategoryCode());
		return dto;
	}

	private Scale mapScale(CropTrait trait) {
		if (trait == null) return null;
		var dto = new Scale();
		dto.setScaleDbId(map(trait.getId()));
		dto.setDataType(mapDataType(trait));
//		dto.setDecimalPlaces(trait.)
		dto.setScaleName(trait.getCodedName());
//		dto.setUnits(trait.getUom());
		if (Objects.equals("Y", trait.getIsCoded())) dto.setValidValues(mapValidValues(trait));
		return dto;
	}

	private Scale mapScale(TranslatedCropTrait trait) {
		if (trait == null) return null;
		var dto = mapScale(trait.entity);
		dto.setScaleName(trait.title);
		if (Objects.equals("Y", trait.entity.getIsCoded())) dto.setValidValues(mapValidValues(trait));
		return dto;
	}

	private DataType mapDataType(CropTrait trait) {
		if (Objects.equals("Y", trait.getIsCoded())) {
			return Scale.DataType.NOMINAL;
		}

		String dataTypeCode = trait.getDataTypeCode().toUpperCase().trim();
		switch (dataTypeCode) {
		case CommunityCodeValues.DATATYPE_NUMERIC:
			return Scale.DataType.NUMERICAL;
		case CommunityCodeValues.DATATYPE_CHAR:
			return Scale.DataType.TEXT;
		default:
			return Scale.DataType.TEXT;
		}
	}

	private ValidValues mapValidValues(CropTrait trait) {
		var dto = new ValidValues();
		if (Objects.equals("Y", trait.getIsCoded())) {
			dto.setCategories(map(trait.getCodes(), this::map));
		}
		dto.setMinimumValue(map(trait.getNumericMinimum()));
		dto.setMaximumValue(map(trait.getNumericMaximum()));
		if (trait.getNumericMinimum() != null) dto.setMin(trait.getNumericMinimum().intValue());
		if (trait.getNumericMaximum() != null) dto.setMax(trait.getNumericMaximum().intValue());
		return dto;
	}

	private ValidValues mapValidValues(TranslatedCropTrait trait) {
		var dto = mapValidValues(trait.entity);
		if (Objects.equals("Y", trait.entity.getIsCoded())) {
			dto.setCategories(map(trait.codes, this::map));
		}
		return dto;
	}

	private Category map(CropTraitCode data) {
		var dto = new Category();
		dto.setValue(data.getCode());
		dto.setLabel(data.getCode());
		return dto;
	}

	private Category map(TranslatedCropTraitCode data) {
		var dto = new Category();
		dto.setValue(data.entity.getCode());
		dto.setLabel(data.title);
		return dto;
	}

	public CropTrait map(ObservationVariable variable) {
		return null;
	}

}