package eu.dnetlib.pid.service;

import java.io.StringReader;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.socialhistoryservices.pid.LocationType;
import org.socialhistoryservices.pid.PidType;

import com.google.common.collect.Lists;

import eu.dnetlib.miscutils.cache.EhCache;
import eu.dnetlib.miscutils.functional.UnaryFunction;
import eu.dnetlib.pid.service.rmi.PIDBridgeService;

/**
 * This class implements a UnaryFunction for the assignment of PIDs in HOPE metadata records.
 * <p>
 * There are three main cases to be managed depending on the structure of the rules to be applied, please see
 * hope_pid_rules.xml in cnr-hope-profiles module for rule details:
 * </p>
 * <p>
 * Given a localIdentifier:
 * <ul>
 * <li>if the rule has resolveURL and not locatts: we can upsert the PID of the localID with the given resolveURL</li>
 * <li>if the rule has not resolveURL and not locatts: then it is a rule to be applied to relationship fields (i.e.,
 * references, such as in descriptiveUnit/isRepresentedBy and digitalResource/represents) and we can call the
 * getQuickPid(localID) if and only if the localID is not already bound to any PIDs, otherwise we might wrongly reset
 * previously created binding to resolveURL and locatts</li>
 * <li>if the rule has no resolveURL and does have locatts, then the rule refers to a digitalResource entity. In this
 * case, the behaviour depends on weather the current digital resource has a non empty value in the element identified
 * by the resolveURL xpath specified by the locatts. Namely, if the record has a non empty value, then we are in the
 * case of a CP using a LOR, hence the aggregator must update its locatts (i.e.derivative 2 and 3); if the record has an
 * empty value, then the CP is using the SOR and the aggregator should not do anything to the locatts at all:
 * <ul>
 * <li>resolveURL value not empty: CP uses LOR, hence upsert the PID with locatts;</li>
 * <li>resolveURL value is empty: CP uses SOR, hence: if the record has empty value in the rule target path, then ask by
 * localID, otherwise do nothing and use the PID as it is</li>
 * </ul>
 * </li>
 * </ul>
 * </p>
 * 
 * @author alessia
 * 
 */
public class CachedPIDUnaryFunction implements UnaryFunction<String, String> {

	private static final Log log = LogFactory.getLog(CachedPIDUnaryFunction.class);

	private Collection<PIDAssignmentRule> pidRules;
	private PIDBridgeService pidBridgeService;
	private String namingAuthority = "12345.1";
	private String unavailableURL = "";
	private final String prefix_PID_URL = "http://hdl.handle.net/";
	private final PIDAssignerHelper helper = new PIDAssignerHelper(this.unavailableURL);

	@Resource
	private EhCache<String, PidType> pidCache;

	/**
	 * 
	 * {@inheritDoc}
	 * 
	 * @see eu.dnetlib.miscutils.functional.UnaryFunction#evaluate(java.lang.Object)
	 */
	@Override
	public String evaluate(String record) {
		log.debug("******EVALUATE********");
		try {
			Document doc = (new SAXReader()).read(new StringReader(record));
			Document current = doc;
			for (PIDAssignmentRule r : this.getPidRules()) {
				current = this.evaluate(current, r);
			}
			log.debug("******RECORD EVALUATED****************");
			return current.asXML();
		} catch (DocumentException e) {
			log.fatal("Cannot parse record to assign PID");
			throw new RuntimeException("Cannot parse record to assign PID", e);
		}
	}

	/**
	 * Generated PIDs for entities in the document according to the provided rule
	 * 
	 * @param d
	 *            input XML Document
	 * @param r
	 *            input rule
	 * @return a new Document equals to d but with PIDs
	 */
	protected Document evaluate(final Document d, final PIDAssignmentRule r) {
		Document doc = d;
		String resolveURLPath = r.getResolveURLXPath();
		String objIdentifier = d.selectSingleNode(".//*[local-name()='record']/*[local-name() = 'header']/*[local-name() = 'objIdentifier']").getText();
		log.info("evaluating record objIdentifier: " + objIdentifier);
		Map<String, PIDLocAttRule> locattMap = r.getLocattRules();
		if ((resolveURLPath != null && !resolveURLPath.isEmpty()) && (locattMap == null || locattMap.isEmpty()))
			return this.descriptiveUnitCase(doc, r, objIdentifier);
		if ((resolveURLPath == null || resolveURLPath.isEmpty()) && (locattMap == null || locattMap.isEmpty()))
			return this.referenceCase(doc, r, objIdentifier);
		if ((resolveURLPath == null || resolveURLPath.isEmpty()) && locattMap != null && !locattMap.isEmpty())
			return this.digitalResourceCase(doc, r, objIdentifier);
		return doc;
	}

	private Document descriptiveUnitCase(final Document d, final PIDAssignmentRule r, final String objIdentifier) {
		log.debug("CASE: descriptiveUnit");
		Document doc = d;
		String baseNodesPath = r.getBaseNodesXPath();
		String localIDPath = r.getLocalIDXPath();
		String targetPath = r.getTargetXPath();
		String resolveURLPath = r.getResolveURLXPath();
		@SuppressWarnings("unchecked")
		List<Node> baseNodes = doc.selectNodes(baseNodesPath);
		for (Node n : baseNodes) {
			try {
				if (this.helper.isNotBlank(n, localIDPath, objIdentifier)) {
					Node localIDNode = n.selectSingleNode(localIDPath);
					String localID = localIDNode.getText();
					Node targetNode = n.selectSingleNode(targetPath);
					if (targetNode == null) {
						log.fatal("Expected target node at path: " + targetPath + " in record " + objIdentifier + " not found. PID not generated.");
						break;
					}
					PidType pid = this.getFromCache(localID);
					if (pid != null) {
						log.info("Got pid from cache: " + pid.getPid() + " for localID: " + localID);
					} else {
						String resolveURL = this.helper.getResolveURL(n, resolveURLPath);
						pid = this.upsertAndCachePID(localID, resolveURL, null);
						log.debug("upsert and cached pid " + pid.getPid() + " for localID " + localID);
					}
					this.assign(n, pid, targetPath, r.getLocattRules());
				}
			} catch (Exception e) {
				log.fatal("Cannot assign a PID for descriptive unit in record: " + objIdentifier + " at path: " + targetPath + " .\nCause: \n" + e);
				e.printStackTrace();
			}
		}
		return doc;
	}

	private Document referenceCase(final Document d, final PIDAssignmentRule r, final String objIdentifier) {
		log.debug("CASE: reference");
		Document doc = d;
		String baseNodesPath = r.getBaseNodesXPath();
		String localIDPath = r.getLocalIDXPath();
		String targetPath = r.getTargetXPath();
		@SuppressWarnings("unchecked")
		List<Node> baseNodes = doc.selectNodes(baseNodesPath);
		for (Node n : baseNodes) {
			try {
				if (this.helper.isNotBlank(n, localIDPath, objIdentifier)) {
					String localID = n.selectSingleNode(localIDPath).getText();
					Node targetNode = n.selectSingleNode(targetPath);
					if (targetNode == null) {
						log.fatal("Expected target node at path: " + targetPath + " not found in record " + objIdentifier + ". PID not generated.");
						break;
					}
					if (this.helper.isBlank(targetNode)) {
						log.debug("PID to be fetched...from cache, getByAttribute or getQuickPid?");
						PidType pid = this.getFromCache(localID);
						if (pid != null) {
							log.info("Got pid from cache: " + pid.getPid() + " for localID: " + localID);
						} else {
							pid = this.getPIDByAttribute(localID);
							if (pid == null) {
								pid = this.pidBridgeService.getQuickPid(namingAuthority, localID, "");
								log.debug(localID + " --> getquick pid: " + pid.getPid());
							} else {
								log.debug(localID + " --> got PID by localID attribute: " + pid.getPid());
							}
						}
						this.assign(n, pid, targetPath, r.getLocattRules());
					} else {
						log.debug(localID + " --> PID already there: nothing to do" + targetNode.getText());
					}
				}
			} catch (Exception e) {
				log.fatal("Cannot assign a referenced PID in record: " + objIdentifier + " at path: " + targetPath + " .\nCause:\n" + e);
				e.printStackTrace();
			}
		}
		return doc;
	}

	private Document digitalResourceCase(final Document d, final PIDAssignmentRule r, final String objIdentifier) {
		log.debug("CASE: digitalResource");
		Document doc = d;
		String baseNodesPath = r.getBaseNodesXPath();
		String localIDPath = r.getLocalIDXPath();
		String targetPath = r.getTargetXPath();
		@SuppressWarnings("unchecked")
		List<Node> baseNodes = doc.selectNodes(baseNodesPath);
		for (Node n : baseNodes) {
			try {
				if (this.helper.isNotBlank(n, localIDPath, objIdentifier)) {
					String localID = n.selectSingleNode(localIDPath).getText();
					Node targetNode = n.selectSingleNode(targetPath);
					if (targetNode == null) {
						log.fatal("Expected target node at path: " + targetPath + " not found in record " + objIdentifier + ". PID not generated.");
						break;
					}
					PidType pid = this.getFromCache(localID);
					if (pid != null) {
						log.info("Got pid from cache: " + pid.getPid() + " for localID: " + localID);
						this.assign(n, pid, targetPath, r.getLocattRules());
					} else {
						pid = this.getPIDByAttribute(localID);
						if (this.helper.isNotBlank(n, r.getLocattRules().get("level2").getResolveURLXPath(), objIdentifier)) {
							log.debug(localID + " --> case with LOR");
							//case with LOR --> upsert and cache
							List<LocationType> locatts = this.helper.getLocationTypeList(r, n);
							//we need to set something for the resolveURL of a digital resource!
							pid = this.upsertAndCachePID(localID, this.helper.getDerivative2URL(locatts), locatts);
							this.assign(n, pid, targetPath, r.getLocattRules());
						} else {
							log.debug(localID + " --> case with SOR");
							if (this.helper.isBlank(targetNode)) {
								//case with SOR --> getByAttr and use it if there is a PID, otherwise getQuickPid
								if (pid == null) {
									pid = this.pidBridgeService.getQuickPid(namingAuthority, localID, "");
									this.helper.setEmptyLocatts(pid);
									log.debug(localID + " --> getquick pid: " + pid.getPid());
								} else
									log.debug(localID + " --> got PID by localID attribute: " + pid.getPid());
								this.putInCache(localID, pid);
								this.assign(n, pid, targetPath, r.getLocattRules());
							} else {
								log.debug(localID + " --> PID already there: nothing to do" + targetNode.getText());
							}
						}
					}
				}
			} catch (Exception e) {
				log.fatal("Cannot assign a PID in digitalResource: " + objIdentifier + " at path: " + targetPath + " .\nCause: " + e);
				e.printStackTrace();
			}
		}
		return doc;
	}

	private PidType getPIDByAttribute(String attribute) {
		List<PidType> matching = this.pidBridgeService.getPidByAttribute(namingAuthority, attribute);
		if (matching.size() == 0) {
			return null;
		} else {
			if (matching.size() > 1) {
				log.fatal("received more than one pid asking for attribute value, considering the first returned PID for " + attribute);
				log.fatal(matching);
				this.removePIDS(matching.subList(1, matching.size()));
			}
			PidType pid = matching.get(0);
			if (pid.getLocAtt() == null) {
				this.helper.setEmptyLocatts(pid);
			}
			return pid;
		}
	}

	private void removePIDS(List<PidType> pidsToRemove) {
		log.fatal("Now deleting other pids");
		this.pidBridgeService.deleteListPids(pidsToRemove);
	}

	/**
	 * Assigns the pid to a target node. Location attributes are also set according to the provided location rules, if
	 * any.
	 * 
	 * @param baseNode
	 *            current Node
	 * @param pidType
	 *            PID
	 * @param targetXPath
	 *            path to the target node for the pid url
	 * @param locattRules
	 *            location rules, can be null or empty
	 */
	private void assign(Node baseNode, PidType pidType, String targetXPath, Map<String, PIDLocAttRule> locattRules) {
		String pidURL = this.createFullURL(pidType.getPid());
		baseNode.selectSingleNode(targetXPath).setText(pidURL);
		if (locattRules != null && !locattRules.isEmpty()) {
			if (pidType.getLocAtt() == null) {
				log.fatal("LocattRules: " + locattRules);
				log.fatal("Unexpected null locations for PID: " + pidType.getPid() + " : can't assign derivative2 and derivative3 PIDs");
				new Exception().printStackTrace();
			} else
				this.assignLocs(baseNode, pidType, locattRules);
		}
	}

	private void assignLocs(Node baseNode, PidType pidType, Map<String, PIDLocAttRule> locattRules) {
		PIDLocAttRule derivative2rule = locattRules.get("level2");
		PIDLocAttRule derivative3rule = locattRules.get("level3");
		this.assignLoc(baseNode, pidType, derivative2rule, "level2");
		this.assignLoc(baseNode, pidType, derivative3rule, "level3");

		//		for (LocationType lt : pidType.getLocAtt().getLocation()) {
		//			if (lt != null && lt.getView() != null && !lt.getView().isEmpty()) {
		//				PIDLocAttRule rule = locattRules.get(lt.getView());
		//				//skip views different from those conceived in our profile
		//				if (rule != null) {
		//					String ltarget = rule.getTargetXPath();
		//					String lresURL = rule.getResolveURLXPath();
		//					String locattURL = this.createFullURL(pidType.getPid() + "?locatt=view:" + lt.getView());
		//					baseNode.selectSingleNode(ltarget).setText(locattURL);
		//					baseNode.selectSingleNode(lresURL).setText(lt.getHref());
		//					log.debug("assigned locatt " + locattURL + " resolving to: " + lt.getHref() + " in relative xpath: " + ltarget + " baseNode is: "
		//							+ baseNode.getPath());
		//				}
		//			}
		//		}
	}

	private void assignLoc(Node baseNode, PidType pidType, PIDLocAttRule rule, String view) {
		if (rule == null) {
			log.fatal("PIDLocattRule is null: can't assign derivative " + view + " for PID: " + pidType + " in the record");
			return;
		}
		String ltarget = rule.getTargetXPath();
		String locattURL = this.createFullURL(pidType.getPid() + "?locatt=view:" + view);
		baseNode.selectSingleNode(ltarget).setText(locattURL);
		log.debug("assigned locatt " + locattURL + " in relative xpath: " + ltarget + " baseNode is: " + baseNode.getPath());
	}

	private PidType upsertAndCachePID(String localID, String resolveURL, List<LocationType> locationTypes) {
		PidType pid = this.pidBridgeService.upsert(namingAuthority, "", resolveURL, localID, locationTypes);
		this.putInCache(localID, pid);
		return pid;
	}

	private PidType getFromCache(String localID) {
		return this.pidCache.get(this.namingAuthority + "_" + localID);
	}

	private void putInCache(String localID, PidType pid) {
		if (pid == null)
			log.fatal("I am not storing a null PID in the cache for " + localID);
		else
			this.pidCache.put(this.namingAuthority + "_" + localID, pid);
	}

	private String createFullURL(String suffix) {
		return prefix_PID_URL + suffix;
	}

	/**
	 * Init rules' collection.
	 */
	public CachedPIDUnaryFunction() {
		this.pidRules = Lists.newArrayList();
	}

	/**
	 * Init rules' collection.
	 */
	public CachedPIDUnaryFunction(Collection<PIDAssignmentRule> rules) {
		this.pidRules = rules;
	}

	public void addRule(PIDAssignmentRule r) {
		pidRules.add(r);
	}

	public void setPidRules(Collection<PIDAssignmentRule> pidRules) {
		this.pidRules = pidRules;
	}

	public Collection<PIDAssignmentRule> getPidRules() {
		return pidRules;
	}

	public String getNamingAuthority() {
		return namingAuthority;
	}

	public void setNamingAuthority(String namingAuthority) {
		this.namingAuthority = namingAuthority;
	}

	public PIDBridgeService getPidBridgeService() {
		return pidBridgeService;
	}

	public void setPidBridgeService(PIDBridgeService pidBridgeService) {
		this.pidBridgeService = pidBridgeService;
	}

	public String getUnavailableURL() {
		return unavailableURL;
	}

	public void setUnavailableURL(String unavailableURL) {
		this.unavailableURL = unavailableURL;
	}

	public EhCache<String, PidType> getPidCache() {
		return pidCache;
	}

	public void setPidCache(EhCache<String, PidType> pidCache) {
		this.pidCache = pidCache;
	}

}
