

package gov.va.med.cds.persistence.hibernate;


import gov.va.med.cds.clinicaldata.vhim400.ClinicalDataResponseInterface;
import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.filter.EntryFilterInterface;
import gov.va.med.cds.persistence.QueryAssociationInterface;
import gov.va.med.cds.persistence.QueryWorkInterface;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;

import java.util.List;


public class HibernateDomModelAssembler
    implements
        ModelAssemblerInterface
{
    protected ClinicalDataResponseInterface clinicalDataResponse;

    protected List<String> removes;

    protected List<String> emptyElementList;


    @SuppressWarnings( "unchecked" )
    @Override
    public Document assembleModel( EntryFilterInterface entryFilter, List<QueryWorkInterface> queryWork )
    {
        Document clinicalDataDocument = clinicalDataResponse.buildEmptyClinicalDocumentWithPatient( entryFilter.getTemplateId(), entryFilter.getRequestId() );
        Element clinicalRootElement = clinicalDataResponse.getClinicalRoot( clinicalDataDocument );

        // iterate over the query work assembling its results into the model
        for ( QueryWorkInterface work : queryWork )
        {
            QueryAssociationInterface queryAssociation = work.getQueryAssociation();
            String parentKey = queryAssociation.getParentKey();

            Element results = ( Element )work.getResults();
            if ( results != null && results.hasContent() )
            {
                // iterate over each result assembling it into the model
                for ( Element childElement : ( List<Element> )results.elements() )
                {
                    // build an xpath for locating the parent element of this result
                    String parentKeyValue = ( parentKey == null ) ? null : childElement.elementText( parentKey );
                    String xpath = null;

                    if ( parentKeyValue != null )
                    {
                        // if its an association with an identifier, build the xpath with a predicate clause.
                        //                        xpath = String.format( "//%s[%s='%s']", queryAssociation.getAssociationParent(), queryAssociation.getKey(), parentKeyValue );
                        xpath = String.format( "%s/%s[%s='%s']", clinicalRootElement.getUniquePath(), queryAssociation.getAssociationParent(), queryAssociation.getKey(), parentKeyValue );
                    }
                    else
                    {
                        // if its an association without an identifier (i.e. patient -> entry_point), build the xpath without a predicate.
                        //                        xpath = String.format( "//%s", queryAssociation.getAssociationParent() );
                        xpath = clinicalRootElement.getUniquePath();
                    }

                    // select the parent element from the model using the xpath
                    List<Node> nodeList = clinicalRootElement.selectNodes( xpath );

                    Node node = null;

                    if ( null != nodeList )
                    {
                        node = nodeList.get( 0 );
                    }

                    if ( node != null && node instanceof Element )
                    {
                        Element parent = ( Element )node;
                        if ( queryAssociation.isCollapsable() )
                        {
                            // check to see if the element already exists in the parent.
                            Element collapseToElement = parent.element( childElement.getName() );
                            if ( collapseToElement != null )
                            {
                                for ( Node n : ( ( List<Node> )childElement.elements() ) )
                                {
                                    collapseToElement.add( n.detach() );
                                }

                                childElement = collapseToElement;
                            }
                            else
                            {
                                parent.add( childElement.detach() );
                            }
                        }
                        else
                        {
                            parent.add( ( Element )childElement.detach() );
                        }
                    }
                    else
                    {
                        // for some reason the query result cannot be assembled into the model.
                        throw new ModelAssemblerException( ErrorCodeEnum.MODEL_ASSEMBLER_EXCEPTION_NOT_ALL_RESULTS_ASSEMBLED, entryFilter.getDomainEntryPoint(), work.getQueryAssociation()
                                        .getAssociationParent() );
                    }
                }
            }
        }

        // finally, clean up the model by removing all of the extraneous 
        // id elements from it.
        removeElements( clinicalRootElement );

        // remove empty elements
        if ( emptyElementList != null )
        {
            removeEmptyElements( clinicalRootElement );
        }

        return clinicalDataDocument;
    }


    /**
     * Removes the element(s) that satisfy the xpath expression passed in on the xpath
     * parameter from the parent element.
     * @param parent The element having the xpath expression applied to it.
     * @param xpath The xpath expression identifying nodes to be removed from the parent element.
     */
    @SuppressWarnings( "unchecked" )
    protected void removeElements( Element parent )
    {
        for ( String xpath : removes )
        {
            List<Element> elements = parent.selectNodes( xpath );
            if ( elements != null && elements.size() > 0 )
            {
                for ( Element element : elements )
                {
                    element.detach();
                }
            }
        }
    }


    /**
     * Removes the empty element(s) that satisfy the xpath expression passed in on the xpath
     * parameter from the parent element.
     * @param parent The element having the xpath expression applied to it.
     * @param xpath The xpath expression identifying nodes to be removed from the parent element.
     */
    @SuppressWarnings( "unchecked" )
    protected void removeEmptyElements( Element parent )
    {
        for ( String xpath : emptyElementList )
        {
            List<Element> elements = parent.selectNodes( xpath );
            if ( elements != null && elements.size() > 0 )
            {
                for ( Element element : elements )
                {
                    if ( element.getText() == null || element.getText().equals( "" ) )
                    {
                        element.getParent().getParent().detach();
                    }
                }
            }
        }
    }


    /**
     * Sets the clinical data response implementation that is needed for building 
     * a response document.
     * @param clinicalDataResponse The clinical data response.
     */
    public void setClinicalDataResponse( ClinicalDataResponseInterface clinicalDataResponse )
    {
        this.clinicalDataResponse = clinicalDataResponse;
    }


    /**
     * Set the list of XPaths to be used by this model assembler to remove unwanted 
     * elements.
     * @param removes
     */
    public void setRemoves( List<String> removes )
    {
        this.removes = removes;
    }


    /**
     * Set the list of XPaths to be used by this model assembler to remove empty elements.
     * @param removes
     */
    public void setEmptyElementList( List<String> emptyElementList )
    {
        this.emptyElementList = emptyElementList;
    }
}
