/*
 * #%L
 * Protempa Framework
 * %%
 * 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 org.protempa.dest.table;

import java.io.IOException;
import java.io.Writer;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import org.apache.commons.lang3.ArrayUtils;

import org.apache.commons.lang3.StringUtils;
import org.arp.javautil.string.StringUtil;
import org.protempa.KnowledgeSource;
import org.protempa.KnowledgeSourceCache;
import org.protempa.KnowledgeSourceReadException;
import org.protempa.PropertyDefinition;
import org.protempa.PropositionDefinition;
import org.protempa.ProtempaUtil;
import org.protempa.proposition.AbstractParameter;
import org.protempa.proposition.Constant;
import org.protempa.proposition.Context;
import org.protempa.proposition.Event;
import org.protempa.proposition.PrimitiveParameter;
import org.protempa.proposition.Proposition;
import org.protempa.proposition.UniqueId;
import org.protempa.proposition.value.Value;
import org.protempa.proposition.visitor.AbstractPropositionVisitor;
import org.protempa.valueset.ValueSet;

public class PropositionColumnSpec extends AbstractTableColumnSpec {

    private static final ThreadLocal<NumberFormat> numberFormat = new ThreadLocal<NumberFormat>() {
        @Override
        protected NumberFormat initialValue() {
            return NumberFormat.getInstance();
        }
    };
    private final Link[] links;
    private final String[] propertyNames;
    private final int numInstances;
    private final String columnNamePrefixOverride;
    private final OutputConfig outputConfig;
    private final ValueOutputConfig valueOutputConfig;
    private final ValuesPropositionVisitor propositionVisitor;

    public PropositionColumnSpec(String[] propertyNames) {
        this(propertyNames, null, null);
    }

    public PropositionColumnSpec(String columnNamePrefixOverride,
            String[] propertyNames) {
        this(columnNamePrefixOverride, propertyNames, null, null, null, 1);
    }

    public PropositionColumnSpec(String[] propertyNames,
            OutputConfig outputConfig, ValueOutputConfig valueOutputConfig) {
        this(propertyNames, outputConfig, valueOutputConfig, null);
    }

    public PropositionColumnSpec(String[] propertyNames,
            OutputConfig outputConfig, ValueOutputConfig valueOutputConfig,
            Link[] links) {
        this(propertyNames, outputConfig, valueOutputConfig, links, 1);
    }

    public PropositionColumnSpec(String[] propertyNames,
            OutputConfig outputConfig, ValueOutputConfig valueOutputConfig,
            Link[] links, int numInstances) {
        this(null, propertyNames, outputConfig, valueOutputConfig, links,
                numInstances);
    }

    public PropositionColumnSpec(String columnNamePrefixOverride,
            String[] propertyNames, OutputConfig outputConfig,
            ValueOutputConfig valueOutputConfig, Link[] links, int numInstances) {
        if (propertyNames == null) {
            this.propertyNames = ArrayUtils.EMPTY_STRING_ARRAY;
        } else {
            ProtempaUtil.checkArrayForNullElement(propertyNames,
                    "propertyNames");
            this.propertyNames = propertyNames.clone();
            ProtempaUtil.internAll(this.propertyNames);
        }

        if (outputConfig == null) {
            this.outputConfig = new OutputConfig();
        } else {
            this.outputConfig = outputConfig;
        }

        if (valueOutputConfig == null) {
            this.valueOutputConfig = (new ValueOutputConfigBuilder()).build();
        } else {
            this.valueOutputConfig = valueOutputConfig;
        }

        if (links == null) {
            this.links = Util.EMPTY_LINK_ARRAY;
        } else {
            ProtempaUtil.checkArrayForNullElement(links, "links");
            this.links = links.clone();
        }

        if (numInstances < 1) {
            throw new IllegalArgumentException("numInstances cannot be < 1");
        }
        this.numInstances = numInstances;
        this.columnNamePrefixOverride = columnNamePrefixOverride;
        propositionVisitor = new ValuesPropositionVisitor();
    }

    protected String[] columnNames(String prefix) {
        List<String> results = new ArrayList<>();
        if (this.outputConfig.showId()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getIdHeading(),
                    this.columnNamePrefixOverride + "_id"));
        }
        if (this.outputConfig.showValue()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getValueHeading(),
                    this.columnNamePrefixOverride + "_value"));
        }
        if (this.outputConfig.showDisplayName()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getDisplayNameHeading(),
                    this.columnNamePrefixOverride + "_displayName"));
        }
        if (this.outputConfig.showAbbrevDisplayName()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getAbbrevDisplayNameHeading(),
                    this.columnNamePrefixOverride + "_abbrevDisplayName"));
        }
        if (this.outputConfig.showStartOrTimestamp()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getStartOrTimestampHeading(),
                    this.columnNamePrefixOverride + "_startOrTimeStamp"));
        }
        if (this.outputConfig.showFinish()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getFinishHeading(),
                    this.columnNamePrefixOverride + "_finish"));
        }
        if (this.outputConfig.showLength()) {
            results.add(StringUtils.defaultIfEmpty(
                    outputConfig.getLengthHeading(),
                    this.columnNamePrefixOverride + "_length"));
        }
        for (String heading : this.propertyNames) {
            results.add(this.columnNamePrefixOverride + "." + heading);
        }
        return results.toArray(new String[results.size()]);
    }

    private class ValuesPropositionVisitor extends AbstractPropositionVisitor {

        private KnowledgeSourceCache ksCache;
        private String[] result;
        private int i = 0;

        ValuesPropositionVisitor() {
        }

        void setKnowledgeSource(KnowledgeSourceCache ksCache) {
            this.ksCache = ksCache;
        }

//        KnowledgeSource getKnowledgeSource() {
//            return this.knowledgeSource;
//        }
        void setResult(String[] result) {
            this.result = result;
        }

        @Override
        public void visit(AbstractParameter abstractParameter) {
            if (outputConfig.showId()) {
                result[i++] = abstractParameter.getId();
            }
            if (outputConfig.showValue()) {
                result[i++] = abstractParameter.getValueFormatted();
            }
            displayNames(abstractParameter);
            if (outputConfig.showStartOrTimestamp()) {
                result[i++] = abstractParameter.getStartFormattedShort();
            }
            if (outputConfig.showFinish()) {
                result[i++] = abstractParameter.getFinishFormattedShort();
            }
            if (outputConfig.showLength()) {
                //result[i++] = abstractParameter.getLengthFormattedShort();
                /*
                 * This is a hack until we have an API in PROTEMPA to get a 
                 * length string without units.
                 */
                result[i++] = numberFormat.get().format(
                        abstractParameter.getInterval().getMinLength());
            }
            processProperties(abstractParameter);
        }

        @Override
        public void visit(Event event) {
            if (outputConfig.showId()) {
                result[i++] = event.getId();
            }
            if (outputConfig.showValue()) {
                result[i++] = null;
            }
            displayNames(event);
            if (outputConfig.showStartOrTimestamp()) {
                result[i++] = event.getStartFormattedShort();
            }
            if (outputConfig.showFinish()) {
                result[i++] = event.getFinishFormattedShort();
            }
            if (outputConfig.showLength()) {
                //result[i++] = event.getLengthFormattedShort();
                /*
                 * This is a hack until we have an API in PROTEMPA to get a 
                 * length string without units.
                 */
                Long minLength = event.getInterval().getMinLength();
                if (minLength != null) {
                    result[i++] = numberFormat.get().format(minLength);
                } else {
                    result[i++] = "";
                }
            }
            processProperties(event);
        }

        @Override
        public void visit(PrimitiveParameter primitiveParameter) {
            if (outputConfig.showId()) {
                result[i++] = primitiveParameter.getId();
            }
            if (outputConfig.showValue()) {
                result[i++] = primitiveParameter.getValueFormatted();
            }
            displayNames(primitiveParameter);
            if (outputConfig.showStartOrTimestamp()) {
                result[i++] = primitiveParameter.getStartFormattedShort();
            }
            if (outputConfig.showFinish()) {
                result[i++] = primitiveParameter.getFinishFormattedShort();
            }
            if (outputConfig.showLength()) {
                //result[i++] = primitiveParameter.getLengthFormattedShort();
                /*
                 * This is a hack until we have an API in PROTEMPA to get a 
                 * length string without units.
                 */
                Long minLength =
                        primitiveParameter.getInterval().getMinLength();
                if (minLength != null) {
                    result[i++] = numberFormat.get().format(minLength);
                } else {
                    result[i++] = "";
                }
            }
            processProperties(primitiveParameter);
        }

        @Override
        public void visit(Constant constant) {
            if (outputConfig.showId()) {
                result[i++] = constant.getId();
            }
            if (outputConfig.showValue()) {
                result[i++] = null;
            }
            displayNames(constant);
            if (outputConfig.showStartOrTimestamp()) {
                result[i++] = null;
            }
            if (outputConfig.showFinish()) {
                result[i++] = null;
            }
            if (outputConfig.showLength()) {
                result[i++] = null;
            }
            processProperties(constant);
        }

        @Override
        public void visit(Context context) {
            throw new UnsupportedOperationException(
                    "Contexts not supported yet");
        }

        void clear() {
            this.result = null;
            this.ksCache = null;
            this.i = 0;
        }

        private void displayNames(Proposition proposition) {
            boolean showDisplayName = outputConfig.showDisplayName();
            boolean showAbbrevDisplayName = outputConfig.showAbbrevDisplayName();
            if (showDisplayName || showAbbrevDisplayName) {
                PropositionDefinition propositionDefinition =
                        ksCache.get(proposition.getId());
                if (propositionDefinition != null) {
                    if (showDisplayName) {
                        result[i++] = propositionDefinition.getDisplayName();
                    }
                    if (showAbbrevDisplayName) {
                        result[i++] =
                                propositionDefinition.getAbbreviatedDisplayName();
                    }
                } else {
                    result[i++] = null;
                    Util.logger().log(Level.WARNING,
                            "Cannot write display name for {0} because it is not in the knowledge source", proposition.getId());
                }
            }
        }

        private String getOutputPropertyValue(Proposition proposition,
                String propertyName, Value propertyValue) {
            String outputValue = null;
            boolean showDisplayName =
                    valueOutputConfig.isShowPropertyValueDisplayName();
            boolean showAbbrevDisplayName =
                    valueOutputConfig.isShowPropertyValueAbbrevDisplayName();
            if (showDisplayName || showAbbrevDisplayName) {
                    PropositionDefinition propositionDef =
                            ksCache.get(proposition.getId());
                    if (propositionDef != null) {
                        PropertyDefinition propertyDef =
                                propositionDef.propertyDefinition(propertyName);
                        ValueSet valueSet =
                                ksCache.getValueSet(propertyDef.getValueSetId());
                        if (valueSet != null) {
                            if (showAbbrevDisplayName) {

                                outputValue = valueSet.abbrevDisplayName(propertyValue);
                            } else if (showDisplayName) {
                                outputValue = valueSet.displayName(propertyValue);
                            }
                        } else {
                            Util.logger().log(Level.WARNING,
                                    "Cannot write value set display name because value set {0} is not in the knowledge source", propertyDef.getValueSetId());
                            outputValue = propertyValue.getFormatted();
                        }
                    } else {
                        Util.logger().log(Level.WARNING,
                                "Cannot write value set display name because proposition {0} is not in the knowledgeSource", proposition.getId());
                        outputValue = propertyValue.getFormatted();
                    }

            } else {
                outputValue = propertyValue.getFormatted();
            }
            return outputValue;
        }

        private void processProperties(Proposition proposition) {
            for (String propertyName : propertyNames) {
                Value value = proposition.getProperty(propertyName);
                if (value != null) {
                    result[i++] = getOutputPropertyValue(proposition,
                            propertyName, value);
                } else {
                    result[i++] = null;
                }
            }
        }
    }

    @Override
    public void columnValues(String key, Proposition proposition,
            Map<Proposition, List<Proposition>> forwardDerivations,
            Map<Proposition, List<Proposition>> backwardDerivations,
            Map<UniqueId, Proposition> references,
            KnowledgeSourceCache propDefCache, Map<String, String> replace,
            char delimiter, Writer writer) throws IOException {
        Collection<Proposition> propositions = this.traverseLinks(this.links,
                proposition, forwardDerivations, backwardDerivations,
                references, propDefCache);
        propositionVisitor.setKnowledgeSource(propDefCache);
        String[] result = new String[(this.outputConfig.numActiveColumns()
                + this.propertyNames.length) * this.numInstances];
        propositionVisitor.setResult(result);
        int i = 0;
        for (Proposition prop : propositions) {
            if (i < this.numInstances) {
                prop.accept(propositionVisitor);
                i++;
            } else {
                break;
            }
        }
        propositionVisitor.clear();
        
        StringUtil.escapeAndWriteDelimitedColumns(result, replace, delimiter, writer);
    }

    @Override
    public String[] columnNames(KnowledgeSource knowledgeSource)
            throws KnowledgeSourceReadException {
        String headerString = this.columnNamePrefixOverride != null ? this.columnNamePrefixOverride
                : generateLinksHeaderString(this.links);
        String[] one = columnNames(headerString);
        String[] result = new String[one.length * this.numInstances];
        for (int i = 0; i < result.length; i++) {
            result[i] = one[i % one.length];
        }
        return result;
    }

    @Override
    public void validate(KnowledgeSource knowledgeSource) throws
            TableColumnSpecValidationFailedException,
            KnowledgeSourceReadException {
        int i = 1;
        for (Link link : this.links) {
            try {
                link.validate(knowledgeSource);
            } catch (LinkValidationFailedException ex) {
                throw new TableColumnSpecValidationFailedException(
                        "Validation of link " + i + " failed", ex);
            }
            i++;
        }
    }

    public Link[] getLinks() {
        return links;
    }

    public String[] getPropertyNames() {
        return propertyNames;
    }

    public int getNumInstances() {
        return numInstances;
    }

    public String getColumnNamePrefixOverride() {
        return columnNamePrefixOverride;
    }

    public OutputConfig getOutputConfig() {
        return outputConfig;
    }

    public ValueOutputConfig getValueOutputConfig() {
        return valueOutputConfig;
    }

    public ValuesPropositionVisitor getPropositionVisitor() {
        return propositionVisitor;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((columnNamePrefixOverride == null) ? 0 : columnNamePrefixOverride.hashCode());
        result = prime * result + Arrays.hashCode(links);
        result = prime * result + numInstances;
        result = prime * result + ((outputConfig == null) ? 0 : outputConfig.hashCode());
        result = prime * result + Arrays.hashCode(propertyNames);
        result = prime * result + ((valueOutputConfig == null) ? 0 : valueOutputConfig.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        PropositionColumnSpec other = (PropositionColumnSpec) obj;
        if (columnNamePrefixOverride == null) {
            if (other.columnNamePrefixOverride != null) {
                return false;
            }
        } else if (!columnNamePrefixOverride.equals(other.columnNamePrefixOverride)) {
            return false;
        }
        if (!Arrays.equals(links, other.links)) {
            return false;
        }
        if (numInstances != other.numInstances) {
            return false;
        }
        if (outputConfig == null) {
            if (other.outputConfig != null) {
                return false;
            }
        } else if (!outputConfig.equals(other.outputConfig)) {
            return false;
        }
        if (!Arrays.equals(propertyNames, other.propertyNames)) {
            return false;
        }
        if (valueOutputConfig == null) {
            if (other.valueOutputConfig != null) {
                return false;
            }
        } else if (!valueOutputConfig.equals(other.valueOutputConfig)) {
            return false;
        }
        return true;
    }
    
    @Override
    public String[] getInferredPropositionIds(KnowledgeSource knowledgeSource,
            String[] inPropIds) throws KnowledgeSourceReadException {
        Set<String> result = new HashSet<>();
        for (Link link : this.links) {
            inPropIds = link.getInferredPropositionIds(knowledgeSource, 
                    inPropIds);
            org.arp.javautil.arrays.Arrays.addAll(result, inPropIds);
        }
        return result.toArray(new String[result.size()]);
    }
}
