package uk.me.nxg.unity;

import java.util.List;


/**
 * Represents a function of a unit, such as <code>log(m)</code>.  This consists of a function (‘log’'
 * in this case) and an operand (‘m’' in this case).  The interpretation of <code>log(m)</code> is
 * that it represents the log of the dimensionless number obtained by dividing a quantity (which
 * presumably has the dimensions of length) by the quantity <em>1 m</em>.
 *
 * <p>The functions {@link #getBaseUnitName},
 * {@link #getBaseUnitDefinition}, and {@link #getBaseUnitString}, apply to the
 * operand.  In particular, they give the information about the first
 * unit in the function operand, even if it has multiple units (such
 * as <code>log(V^2/mm)</code>.  This isn't ideal, but it's probably
 * roughly what is wanted in the majority of cases where this function
 * is used.
 *
 * <p>The functions {@link #isRecognisedUnit} and {@link #isRecommendedUnit} are true if they
 * would be true when applied to the operand <em>and</em> the function is a recognised in this
 * syntax; there are no usage constraints on functions, so the function
 * {@link #satisfiesUsageConstraints} is true if it would be true when applied to the operand.
 */
public class FunctionOfUnit
        extends OneUnit {
    private final String functionName;
    private final FunctionDefinition functionDefinition;
    private final UnitExpr operand;

    private FunctionDefinition logFunction = null;
    

    FunctionOfUnit(String functionName, List<OneUnit>operandList)
            throws IllegalArgumentException {
        super((float)1.0, false);
        this.functionName = functionName;
        this.functionDefinition = null;
        if (operandList.isEmpty()) {
            // the grammar should make this impossible
            throw new IllegalArgumentException("FunctionOfUnit has an empty list as argument");
        }
        operand = new UnitExpr(0.0, operandList, null);
        assert !isQuoted();
    }
    FunctionOfUnit(FunctionDefinition functionDefinition, List<OneUnit>operandList)
            throws IllegalArgumentException {
        super((float)1.0, false);
        this.functionName = null;
        this.functionDefinition = functionDefinition;
        if (operandList.isEmpty()) {
            // the grammar should make this impossible
            throw new IllegalArgumentException("FunctionOfUnit has an empty list as argument");
        }
        operand = new UnitExpr(0.0, operandList, null);
        assert !isQuoted();
    }
    private FunctionOfUnit(float exponent,
                           String functionName,
                           FunctionDefinition functionDefinition,
                           UnitExpr operand) {
        super(exponent, false);
        this.functionName = functionName;
        this.functionDefinition = functionDefinition;
        this.operand = operand;
        assert !isQuoted();
    }
    
    @Override public int getPrefix() {
        return 0;
    }
    
    @Override public Dimensions getDimensions() {
        return Dimensions.unity();
    }

    // XXX return the baseunitdefinition of the first unit in the
    // operand (this will be what's wanted in the most common case of
    // there being only one)
    @Override public UnitDefinition getBaseUnitDefinition() {
        return operand.getUnit(0).getBaseUnitDefinition();
    }

    @Override public String getBaseUnitName() {
        return operand.getUnit(0).getBaseUnitName();
    }

    @Override public String getBaseUnitString() {
        return operand.getUnit(0).getBaseUnitString();
    }

    @Override UnitDefinitionMap.Resolver getUnitResolver() {
        return operand.getUnitResolver();
    }

    /** Returns the unit in the operand which corresponds to the required unit. */
    public OneUnit getUnit(UnitDefinition reqUnit) {
        return operand.getUnit(reqUnit);
    }

    @Override public boolean isRecognisedUnit(Syntax syntax) {
        return functionDefinition != null && operand.allUnitsRecognised(syntax);
    }

    @Override public boolean isRecommendedUnit(Syntax syntax) {
        if (functionDefinition == null)
            return false;
        return operand.allUnitsRecommended(syntax);
    }

    @Override public boolean satisfiesUsageConstraints(Syntax syntax) {
        return operand.allUsageConstraintsSatisfied(syntax);
    }

    @Override public String toDebugString() {
        String name;
        
        if (functionDefinition == null) {
            name = functionName;
        } else {
            name = functionDefinition.name();
        }
        if (name == null) {
            name = functionDefinition.fallbackName();
        }
        assert name != null;

        StringBuilder sb = new StringBuilder(name);
        UnitWriterDebugging uwd = new UnitWriterDebugging(operand);
        sb.append('(').append(uwd.write(false)).append(')');
        if (getExponent() != 1) {
            sb.append('^');
            float e = getExponent();
            if (e == Math.floor(e))       // no fractional part
                sb.append(Math.round(e)); // print as integer
            else
                sb.append(e);
        }
        return sb.toString();
    }

    @Override public String toString() {
        return toString(null);
    }

    @Override public String toString(Syntax syntax) {
        try {
            return unitString(syntax);
        } catch (UnitParserException e1) {
            try {
                return unitString(Syntax.FITS);
            } catch (Exception e2) {
                // this should be impossible
                throw new AssertionError("FunctionOfUnit.toString with syntax FITS really should work: " + e2);
            }
        }
    }
    
    @Override public String unitString(Syntax syntax)
            throws UnitParserException {
        String ret;
        if (syntax == Syntax.CDS) {
            ret = unitStringCDS();
        } else if (syntax == Syntax.LATEX) {
            ret = unitStringLaTeX();
        } else {
            ret = unitStringText(syntax);
        }
        assert ret != null;
        return ret;
    }

    String unitStringText(Syntax syntax) 
            throws UnitParserException {
        String name = null;
        if (name == null && functionDefinition != null) {
            // if syntax is null, default to FITS
            name = FunctionDefinitionMap.lookupFunctionName(syntax == null ? Syntax.FITS : syntax,
                                                            functionDefinition);
        }
        if (name == null && functionName != null) {
            name = functionName;
        }
        if (name == null) {
            assert functionDefinition != null;
            name = functionDefinition.toString();
        }
        assert name != null;
        StringBuilder sb = new StringBuilder(name);
        sb.append('(').append(operand.toString()).append(')');
        return sb.toString();
    }

    String unitStringLaTeX()
            throws UnitParserException {
        StringBuilder sb = new StringBuilder();
        sb.append("\\text{\\ensuremath{");
        if (functionName == null) {
            assert(functionDefinition != null);
            sb.append(functionDefinition.latexForm());
        } else {
            assert(functionName != null);
            sb.append("\\mathop{\\mathrm{").append(functionName).append("}}");
        }
        sb.append('(').append(operand.toString(Syntax.LATEX)).append(")}}");
        return sb.toString();
    }
    
    String unitStringCDS()
            throws UnitParserException {
        StringBuilder sb = new StringBuilder();
        if (logFunction == null) {
            logFunction = FunctionDefinitionMap.lookupFunctionDefinition(Syntax.VOUNITS, "log");
            assert logFunction != null;
        }
        
        if (functionDefinition.equals(logFunction)) {
            sb.append('[').append(operand.toString(Syntax.CDS)).append(']');
            return sb.toString();
        } else {
            throw new UnitParserException("CDS syntax can only express log functions, not " + functionDefinition);
        }
    }

    /**
     * The function which is applied to the operand.  If this is not a
     * recognised function in the originating syntax, then this
     * returns null.
     * Either this function or {@link #getFunctionName} returns
     * non-null, and not both.
     */
    public FunctionDefinition getFunctionDefinition() {
        return functionDefinition;
    }

    /**
     * The function which is applied to the operand.  If this is not a
     * recognised function in the originating syntax, then this
     * returns null.
     * Either this function or {@link #getFunctionDefinition} returns
     * non-null, and not both.
     */
    public String getFunctionName() {
        String res;
        if (functionDefinition == null) {
            res = functionName;
        } else {
            assert functionDefinition != null;
            res = functionDefinition.name();
        }
        return res;
    }

    @Override OneUnit reciprocate() {
        return new FunctionOfUnit(-getExponent(), functionName, functionDefinition, operand);
    }

    @Override OneUnit pow(double exponent) {
        return new FunctionOfUnit((float)exponent, functionName, functionDefinition, operand);
    }

    @Override public boolean equals(Object o) {
        if (o instanceof FunctionOfUnit) {
            return compareTo((FunctionOfUnit)o) == 0;
        } else {
            return false;
        }
    }

    @Override public int compareTo(OneUnit o) {
        int cmp;
        if (o == this) {
            cmp = 0;
        } else if (o instanceof SimpleUnit) {
            cmp = +1;           // order SimpleUnit before FunctionOfUnit
        } else {
            FunctionOfUnit u = (FunctionOfUnit)o;
            
            if (functionDefinition != null && u.functionDefinition != null) {
                cmp = functionDefinition.compareTo(u.functionDefinition);
            } else if (functionName != null && u.functionName != null) {
                cmp = functionName.compareTo(u.functionName);
            } else {
                cmp = (functionDefinition != null ? -1 : +1);
            }
            if (cmp == 0) {
                cmp = operand.compareTo(u.operand);
                if (cmp == 0) {
                    // still 
                    double e0 = getExponent();
                    double e1 = u.getExponent();
                    if (e0 == e1) {
                        cmp = 0;
                    } else {
                        cmp = (e0 < e1 ? -1 : +1);
                    }
                }
            }
        }
        return cmp;
    }
}
