/*
 * #%L
 * AIW i2b2 ETL
 * %%
 * Copyright (C) 2012 - 2013 Emory University
 * %%
 * 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.
 * #L%
 */
package edu.emory.cci.aiw.i2b2etl.dest.metadata;

import edu.emory.cci.aiw.i2b2etl.dest.metadata.conceptid.InvalidConceptCodeException;
import edu.emory.cci.aiw.i2b2etl.dest.metadata.conceptid.ModifierParentConceptId;
import edu.emory.cci.aiw.i2b2etl.dest.metadata.conceptid.ConceptId;
import edu.emory.cci.aiw.i2b2etl.dest.metadata.conceptid.ModifierConceptId;
import edu.emory.cci.aiw.i2b2etl.dest.metadata.conceptid.PropDefConceptId;
import edu.emory.cci.aiw.i2b2etl.dest.config.ModifierSpec;
import edu.emory.cci.aiw.i2b2etl.util.Util;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringEscapeUtils;
import org.protempa.Attribute;
import org.protempa.KnowledgeSourceCache;
import org.protempa.KnowledgeSourceReadException;
import org.protempa.ParameterDefinition;
import org.protempa.PropertyDefinition;
import org.protempa.PropositionDefinition;
import org.protempa.proposition.value.Value;
import org.protempa.valueset.ValueSet;
import org.protempa.valueset.ValueSetElement;
import org.protempa.proposition.value.ValueType;

class PropositionConceptTreeBuilder implements OntologyBuilder, SubtreeBuilder {

    private static final ModifierSpec[] EMPTY_MODIFIER_SPEC_ARRAY = new ModifierSpec[0];

    private final SimpleDateFormat valueMetadataCreateDateTimeFormat;
    private final String conceptCode;
    private final Metadata metadata;
    private final ValueTypeCode valueTypeCode;
    private final String createDate;
    private final ModifierSpec[] modifiers;
    private final String[] propIds;
    private final KnowledgeSourceCache knowledgeSourceCache;
    private final boolean alreadyLoaded;
    private List<Concept> roots;

    PropositionConceptTreeBuilder(KnowledgeSourceCache knowledgeSourceCache,
            String[] propIds, String conceptCode, ValueTypeCode valueTypeCode,
            ModifierSpec[] modifiers, boolean alreadyLoaded, Metadata metadata)
            throws KnowledgeSourceReadException,
            UnknownPropositionDefinitionException {
        assert knowledgeSourceCache != null : "knowledgeSourceCache cannot be null";
        assert metadata != null : "metadata cannot be null";
        this.propIds = propIds;
        this.conceptCode = conceptCode;
        this.metadata = metadata;
        this.valueTypeCode = valueTypeCode;
        this.valueMetadataCreateDateTimeFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
        this.createDate = this.valueMetadataCreateDateTimeFormat.format(new Date());
        if (modifiers != null) {
            this.modifiers = modifiers.clone();
        } else {
            this.modifiers = EMPTY_MODIFIER_SPEC_ARRAY;
        }
        this.knowledgeSourceCache = knowledgeSourceCache;
        this.alreadyLoaded = alreadyLoaded;
        this.roots = new ArrayList<>();
    }

    Metadata getMetadata() {
        return metadata;
    }

    @Override
    public void build(Concept concept) throws OntologyBuildException {
        try {
            for (String childPropId : this.propIds) {
                PropositionDefinition childPropDef
                        = this.knowledgeSourceCache.get(childPropId);
                if (childPropDef != null) {
                    Concept child = addNode(childPropDef);
                    if (concept != null) {
                        concept.add(child);
                    }
                    this.roots.add(child);
                    addModifierConcepts(childPropDef, child);
                    buildHelper(childPropDef.getInverseIsA(), child);
                }
            }
        } catch (InvalidConceptCodeException | KnowledgeSourceReadException ex) {
            throw new OntologyBuildException(
                    "Could not build proposition concept tree", ex);
        }
    }

    @Override
    public Concept[] getRoots() {
        return this.roots.toArray(new Concept[this.roots.size()]);
    }

    private void buildHelper(String[] childPropIds, Concept parent)
            throws KnowledgeSourceReadException, InvalidConceptCodeException, OntologyBuildException {
        for (String childPropId : childPropIds) {
            PropositionDefinition childPropDef
                    = this.knowledgeSourceCache.get(childPropId);
            if (childPropDef != null) {
                Concept child = addNode(childPropDef);
                parent.add(child);
                addModifierConcepts(childPropDef, child);
                buildHelper(childPropDef.getInverseIsA(), child);
            }
        }
    }

    private Concept addNode(PropositionDefinition propDef)
            throws InvalidConceptCodeException, KnowledgeSourceReadException, OntologyBuildException {
        ConceptId conceptId
                = PropDefConceptId.getInstance(propDef.getId(), null, null, this.metadata);
        Concept newChild = new Concept(conceptId, this.conceptCode, this.metadata);
        newChild.setDownloaded(propDef.getDownloaded());
        Date updated = propDef.getUpdated();
        newChild.setUpdated(updated != null ? propDef.getUpdated() : propDef.getCreated());
        String[] children = propDef.getChildren();
        String[] inverseIsAs = propDef.getInverseIsA();
        newChild.setInDataSource(children.length == 0 //is a leaf
                || inverseIsAs.length == 0 /* is abstracted */);
        newChild.setDerived(children.length > inverseIsAs.length);
        newChild.setDisplayName(propDef.getDisplayName());
        newChild.setSourceSystemCode(
                MetadataUtil.toSourceSystemCode(
                        propDef.getSourceId().getStringRepresentation()));
        newChild.setValueTypeCode(this.valueTypeCode);
        newChild.setComment(propDef.getDescription());
        newChild.setAlreadyLoaded(this.alreadyLoaded);
        Attribute attribute = propDef.attribute(Util.C_FULLNAME_ATTRIBUTE_NAME);
        if (attribute != null) {
            Value value = attribute.getValue();
            if (value != null) {
                newChild.setFullName(value.getFormatted());
            }
        }
        if (this.valueTypeCode == ValueTypeCode.LABORATORY_TESTS) {
            ValueType valueType
                    = ((ParameterDefinition) propDef).getValueType();
            newChild.setDataType(DataType.dataTypeFor(valueType));
            if (children.length < 1) {
                newChild.setMetadataXml("<?xml version=\"1.0\"?><ValueMetadata><Version>3.02</Version><CreationDateTime>" + this.createDate
                        + "</CreationDateTime><TestID>" + StringEscapeUtils.escapeXml10(newChild.getConceptCode())
                        + "</TestID><TestName>" + StringEscapeUtils.escapeXml10(newChild.getDisplayName())
                        + "</TestName><DataType>" + (newChild.getDataType() == DataType.NUMERIC ? "Float" : "String") + "</DataType><Flagstouse></Flagstouse><Oktousevalues>Y</Oktousevalues><UnitValues><NormalUnits> </NormalUnits></UnitValues></ValueMetadata>");
            }
        } else {
            newChild.setDataType(DataType.TEXT);
        }
        if (this.metadata.getFromIdCache(conceptId) == null) {
            this.metadata.addToIdCache(newChild);
        } else {
            newChild.setSynonymCode(SynonymCode.SYNONYM);
        }
        return newChild;
    }

    private void addModifierConcepts(PropositionDefinition propDef, Concept appliedConcept) throws KnowledgeSourceReadException, InvalidConceptCodeException {
        if (this.modifiers.length > 0) {
            ConceptId modParentId = ModifierParentConceptId.getInstance(propDef.getId(), this.metadata);
            Concept modParent = new Concept(modParentId, null, this.metadata);
            modParent.setAlreadyLoaded(this.alreadyLoaded);
            this.metadata.addModifierRoot(modParent);
            if (this.metadata.getFromIdCache(modParentId) == null) {
                this.metadata.addToIdCache(modParent);
            } else {
                modParent.setSynonymCode(SynonymCode.SYNONYM);
            }
            for (ModifierSpec modifier : this.modifiers) {
                PropertyDefinition propertyDef = propDef.propertyDefinition(modifier.getProperty());
                if (propertyDef != null && !propertyDef.isInherited()) {
                    String valStr = modifier.getValue();
                    Value val = valStr != null ? propertyDef.getValueType().parse(valStr) : null;
                    ConceptId modId = ModifierConceptId.getInstance(
                            propDef.getId(),
                            propertyDef.getId(),
                            val,
                            this.metadata);
                    Concept mod = new Concept(modId, modifier.getCodePrefix(), this.metadata);
                    mod.setDisplayName(modifier.getDisplayName());
                    Date updated = propDef.getUpdated();
                    mod.setUpdated(updated != null ? propDef.getUpdated() : propDef.getCreated());
                    mod.setDownloaded(propDef.getDownloaded());
                    mod.setSourceSystemCode(
                            MetadataUtil.toSourceSystemCode(
                                    propDef.getSourceId().getStringRepresentation()));
                    mod.setValueTypeCode(this.valueTypeCode);
                    mod.setAppliedPath(appliedConcept.getFullName() + "%");
                    mod.setDataType(DataType.dataTypeFor(propertyDef.getValueType()));
                    mod.setFactTableColumn("MODIFIER_CD");
                    mod.setTableName("MODIFIER_DIMENSION");
                    mod.setColumnName("MODIFIER_PATH");
                    mod.setAlreadyLoaded(this.alreadyLoaded);
                    Attribute attribute = propertyDef.getAttribute(Util.C_FULLNAME_ATTRIBUTE_NAME);
                    if (attribute != null) {
                        Value value = attribute.getValue();
                        if (value != null) {
                            mod.setFullName(value.getFormatted());
                        }
                    }
                    if (propertyDef.getValueType() != ValueType.BOOLEANVALUE) {
                        StringBuilder mXml = new StringBuilder();
                        mXml.append("<?xml version=\"1.0\"?><ValueMetadata><Version>3.02</Version><CreationDateTime>");
                        mXml.append(this.createDate);
                        mXml.append("</CreationDateTime><TestID>");
                        mXml.append(StringEscapeUtils.escapeXml10(mod.getConceptCode()));
                        mXml.append("</TestID><TestName>");
                        mXml.append(StringEscapeUtils.escapeXml10(mod.getDisplayName()));
                        mXml.append("</TestName>");
                        String valueSetId = propertyDef.getValueSetId();
                        if (valueSetId != null) {
                            mXml.append("<DataType>Enum</DataType><EnumValues>");
                            ValueSet valueSet = this.knowledgeSourceCache.getValueSet(valueSetId);
                            if (valueSet != null) {
                                for (ValueSetElement vse : valueSet.getValueSetElements()) {
                                    mXml.append("<Val description=\"");
                                    mXml.append(StringEscapeUtils.escapeXml10(vse.getDisplayName()));
                                    mXml.append("\">");
                                    mXml.append(StringEscapeUtils.escapeXml10(vse.getValue().getFormatted()));
                                    mXml.append("</Val>");
                                }
                            }
                            mXml.append("</EnumValues>");
                        } else {
                            mXml.append("<DataType>");
                            mXml.append(mod.getDataType() == DataType.NUMERIC ? "Float" : "String");
                            mXml.append("</DataType>");
                        }
                        mXml.append("<Flagstouse></Flagstouse><Oktousevalues>Y</Oktousevalues><UnitValues><NormalUnits> </NormalUnits></UnitValues></ValueMetadata>");
                        mod.setMetadataXml(mXml.toString());
                    }
                    if (this.metadata.getFromIdCache(modId) == null) {
                        this.metadata.addToIdCache(mod);
                    } else {
                        mod.setSynonymCode(SynonymCode.SYNONYM);
                    }
                    if (this.metadata.getFromIdCache(modId) == null) {
                        this.metadata.addToIdCache(mod);
                    } else {
                        mod.setSynonymCode(SynonymCode.SYNONYM);
                    }
                    modParent.add(mod);
                }
            }
        }
    }

}
