package htsjdk.variant.variantcontext;

import htsjdk.variant.utils.GeneralUtils;
import htsjdk.variant.variantcontext.VariantContextUtils.JexlVCMatchExp;
import htsjdk.variant.vcf.VCFConstants;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.MapContext;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * this is an implementation of a Map of JexlVCMatchExp to true or false values.  It lazy initializes each value
 * as requested to save as much processing time as possible.
 *
 * Compatible with JEXL 1.1 (this code will be easier if we move to 2.0, all of the functionality can go into the
 * JexlContext's get()
 *
 */

class JEXLMap implements Map<JexlVCMatchExp, Boolean> {
    // our variant context and/or Genotype
    private final VariantContext vc;
    private final Genotype g;

    // our context
    private JexlContext jContext = null;

    // our mapping from JEXLVCMatchExp to Booleans, which will be set to NULL for previously uncached JexlVCMatchExp
    private Map<JexlVCMatchExp,Boolean> jexl;


    public JEXLMap(Collection<JexlVCMatchExp> jexlCollection, VariantContext vc, Genotype g) {
        this.vc = vc;
        this.g = g;
        initialize(jexlCollection);
    }

    public JEXLMap(Collection<JexlVCMatchExp> jexlCollection, VariantContext vc) {
        this(jexlCollection, vc, null);
    }

    private void initialize(Collection<JexlVCMatchExp> jexlCollection) {
        jexl = new HashMap<JexlVCMatchExp,Boolean>();
        for (JexlVCMatchExp exp: jexlCollection) {
            jexl.put(exp, null);
        }
    }

    /**
     * create the internal JexlContext, only when required.  This code is where new JEXL context variables
     * should get added.
     *
     */
    private void createContext() {
        if ( g == null ) {
            // todo -- remove dependancy on g to the entire system
            jContext = new VariantJEXLContext(vc);
        } else {
            //
            // this whole branch is here just to support G jexl operations
            //
            Map<String, Object> infoMap = new HashMap<String, Object>();

            if ( vc != null ) {
                // create a mapping of what we know about the variant context, its Chromosome, positions, etc.
                infoMap.put("CHROM", vc.getChr());
                infoMap.put("POS", vc.getStart());
                infoMap.put("TYPE", vc.getType().toString());
                infoMap.put("QUAL", String.valueOf(vc.getPhredScaledQual()));

                // add alleles
                infoMap.put("ALLELES", GeneralUtils.join(";", vc.getAlleles()));
                infoMap.put("N_ALLELES", String.valueOf(vc.getNAlleles()));

                // add attributes
                addAttributesToMap(infoMap, vc.getAttributes());

                // add filter fields
                infoMap.put("FILTER", vc.isFiltered() ? "1" : "0");
                for ( Object filterCode : vc.getFilters() ) {
                    infoMap.put(String.valueOf(filterCode), "1");
                }

                // add genotype-specific fields
                // TODO -- implement me when we figure out a good way to represent this
                //    for ( Genotype g : vc.getGenotypes().values() ) {
                //        String prefix = g.getSampleName() + ".";
                //        addAttributesToMap(infoMap, g.getAttributes(), prefix);
                //        infoMap.put(prefix + "GT", g.getGenotypeString());
                //    }

                // add specific genotype if one is provided
                infoMap.put(VCFConstants.GENOTYPE_KEY, g.getGenotypeString());
                infoMap.put("isHom", g.isHom() ? "1" : "0");
                infoMap.put("isHomRef", g.isHomRef() ? "1" : "0");
                infoMap.put("isHet", g.isHet() ? "1" : "0");
                infoMap.put("isHomVar", g.isHomVar() ? "1" : "0");
                infoMap.put("isCalled", g.isCalled()? "1" : "0");
                infoMap.put("isNoCall", g.isNoCall()? "1" : "0");
                infoMap.put("isMixed", g.isMixed()? "1" : "0");
                infoMap.put("isAvailable", g.isAvailable()? "1" : "0");
                infoMap.put("isPassFT", g.isFiltered()? "0" : "1");
                infoMap.put(VCFConstants.GENOTYPE_FILTER_KEY, g.isFiltered()?  g.getFilters() : "PASS");

                infoMap.put(VCFConstants.GENOTYPE_QUALITY_KEY, g.getGQ());
                if ( g.hasDP() )
                    infoMap.put(VCFConstants.DEPTH_KEY, g.getDP());
                for ( Entry<String, Object> e : g.getExtendedAttributes().entrySet() ) {
                    if ( e.getValue() != null && !e.getValue().equals(VCFConstants.MISSING_VALUE_v4) )
                        infoMap.put(e.getKey(), e.getValue());
                }
            }

            // create the internal context that we can evaluate expressions against

            jContext = new MapContext(infoMap);
        }
    }

    /**
     * @return the size of the internal data structure
     */
    public int size() {
        return jexl.size();
    }

    /**
     * @return true if we're empty
     */
    public boolean isEmpty() { return this.jexl.isEmpty(); }

    /**
     * do we contain the specified key
     * @param o the key
     * @return true if we have a value for that key
     */
    public boolean containsKey(Object o) { return jexl.containsKey(o); }

    public Boolean get(Object o) {
        // if we've already determined the value, return it
        if (jexl.containsKey(o) && jexl.get(o) != null) return jexl.get(o);

        // try and cast the expression
        JexlVCMatchExp e = (JexlVCMatchExp) o;
        evaluateExpression(e);
        return jexl.get(e);
    }

    /**
     * get the keyset of map
     * @return a set of keys of type JexlVCMatchExp
     */
    public Set<JexlVCMatchExp> keySet() {
        return jexl.keySet();
    }

    /**
     * get all the values of the map.  This is an expensive call, since it evaluates all keys that haven't
     * been evaluated yet.  This is fine if you truely want all the keys, but if you only want a portion, or  know
     * the keys you want, you would be better off using get() to get them by name.
     * @return a collection of boolean values, representing the results of all the variants evaluated
     */
    public Collection<Boolean> values() {
        // this is an expensive call
        for (JexlVCMatchExp exp : jexl.keySet())
            if (jexl.get(exp) == null)
                evaluateExpression(exp);
        return jexl.values();
    }

    /**
     * evaulate a JexlVCMatchExp's expression, given the current context (and setup the context if it's null)
     * @param exp the JexlVCMatchExp to evaluate
     */
    private void evaluateExpression(JexlVCMatchExp exp) {
        // if the context is null, we need to create it to evaluate the JEXL expression
        if (this.jContext == null) createContext();
        try {
            final Boolean value = (Boolean) exp.exp.evaluate(jContext);
            // treat errors as no match
            jexl.put(exp, value == null ? false : value);
        } catch (Exception e) {
            // if exception happens because variable is undefined (i.e. field in expression is not present), evaluate to FALSE
            // todo - might be safer if we explicitly checked for an exception type, but Apache's API doesn't seem to have that ability
            if (e.getMessage().contains("undefined variable"))
                jexl.put(exp,false);
            else
                throw new IllegalArgumentException(String.format("Invalid JEXL expression detected for %s with message %s", exp.name, e.getMessage()));
        }
    }

    /**
     * helper function: adds the list of attributes to the information map we're building
     * @param infoMap the map
     * @param attributes the attributes
     */
    private static void addAttributesToMap(Map<String, Object> infoMap, Map<String, ?> attributes ) {
        for (Entry<String, ?> e : attributes.entrySet()) {
            infoMap.put(e.getKey(), String.valueOf(e.getValue()));
        }
    }

    public Boolean put(JexlVCMatchExp jexlVCMatchExp, Boolean aBoolean) {
        return jexl.put(jexlVCMatchExp,aBoolean);
    }

    public void putAll(Map<? extends JexlVCMatchExp, ? extends Boolean> map) {
        jexl.putAll(map);
    }

    // //////////////////////////////////////////////////////////////////////////////////////
    // The Following are unsupported at the moment
    // //////////////////////////////////////////////////////////////////////////////////////

    // this doesn't make much sense to implement, boolean doesn't offer too much variety to deal
    // with evaluating every key in the internal map.
    public boolean containsValue(Object o) {
        throw new UnsupportedOperationException("containsValue() not supported on a JEXLMap");
    }

    // this doesn't make much sense
    public Boolean remove(Object o) {
        throw new UnsupportedOperationException("remove() not supported on a JEXLMap");
    }


    public Set<Entry<JexlVCMatchExp, Boolean>> entrySet() {
        throw new UnsupportedOperationException("clear() not supported on a JEXLMap");
    }

    // nope
    public void clear() {
        throw new UnsupportedOperationException("clear() not supported on a JEXLMap");
    }
}
