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;
    // invariants: operand is non-null,
    // and either functionName or FunctionDefinition (or both) is non-null

    private FunctionDefinition logFunction = null;

    // TODO: it would be good to add @NonNull annotations in front of
    // the arguments to these constructors, but that's not supported
    // in the JDK8 I'm currently supporting.

    /**
     * Create a function-of-unitstring.
     * This indicates the action of the given function on the operand
     * list, for example log(W.s).  The specified function is an
     * 'unknown' one; if it is a known one, then use
     * {@link #FunctionOfUnit(FunctionDefinition,List)} instead.
     *
     * @param functionName the name of the 'unknown' function
     * @param operandList the operands of the function
     */
    FunctionOfUnit(String functionName,
                   List<OneUnit>operandList)
            throws IllegalArgumentException {
        // the master constructor does check that either its second or
        // its third argument is non-null.
        this(1.0f,
             functionName, null,
             new UnitExpr(0.0, operandList, null));
    }

    /**
     * Create a function-of-unitstring, where the operand has a
     * scaling factor.
     * This indicates the action of the given function on the operand
     * list, for example log(1e3W.s).  The specified function is an
     * 'unknown' one; if it is a known one, then use
     * {@link #FunctionOfUnit(FunctionDefinition,List)} instead.
     *
     * @param functionName the name of the 'unknown' function
     * @param operandList the operand of the function
     * @param logscalingfactor the log scaling factor at the front of the overall unit expression
     */
    FunctionOfUnit(String functionName,
                   List<OneUnit>operandList,
                   double logscalingfactor)
            throws IllegalArgumentException {
        this(1.0f,
             functionName, null,
             new UnitExpr(logscalingfactor, operandList, null));
    }

    /**
     * Create a function-of-unitstring.
     * This indicates the action of the given function on the operand
     * list, for example log(W.s).  The specified function is an
     * 'known' one; if it is an unknown one, then use
     * {@link #FunctionOfUnit(String,List)} instead.
     *
     * @param functionDefinition the 'known' function
     * @param operandList the operands of the function
     */
    FunctionOfUnit(FunctionDefinition functionDefinition,
                   List<OneUnit>operandList)
            throws IllegalArgumentException {
        this(1.0f,
             null, functionDefinition,
             new UnitExpr(0.0, operandList, null));
    }

    /**
     * Create a function-of-unitstring, where the operand has a
     * scaling factor.
     * This indicates the action of the given function on the operand
     * list, for example log(1e3W.s).  The specified function is an
     * 'known' one; if it is an unknown one, then use
     * {@link #FunctionOfUnit(String,List)} instead.
     *
     * @param functionDefinition the 'known' function
     * @param operandList the operands of the function
     * @param logscalingfactor the log scaling factor at the front of the overall unit expression
     */
    FunctionOfUnit(FunctionDefinition functionDefinition,
                   List<OneUnit>operandList,
                   double logscalingfactor)
            throws IllegalArgumentException {
        this(1.0f,
             null, functionDefinition,
             new UnitExpr(logscalingfactor, operandList, null));
    }

    private FunctionOfUnit(float exponent,
                           String functionName,
                           FunctionDefinition functionDefinition,
                           UnitExpr operand) {
        super(exponent, false);

        if (functionName == null && functionDefinition == null) {
            throw new IllegalArgumentException("FunctionOfUnit(float,String,FunctionDefinition,UnitExpr): either argument 2 or 3 must be non-null");
        }
        if (operand == null) {
            throw new IllegalArgumentException("FunctionOfUnit(float,String,FunctionDefinition,UnitExpr): argument 4 must be non-null");
        }

        this.functionName = functionName;
        this.functionDefinition = functionDefinition;
        this.operand = operand;

        assert !isQuoted() && (this.functionName != null || this.functionDefinition != null);
    }

    @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.
     * @param reqUnit a unit representing the one we are to extract
     * @return a 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 isRecognisedUnit() {
        return functionDefinition != null && operand.allUnitsRecognised();
    }

    @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 : "toDebugString: function has no name";

        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 UnwritableExpression {
        String ret;
        if (syntax == Syntax.CDS) {
            ret = unitStringCDS();
        } else if (syntax == Syntax.LATEX) {
            ret = unitStringLaTeX();
        } else {
            ret = unitStringText(syntax);
        }
        assert ret != null : String.format("Unable to construct a unit string for syntax [%s]", syntax);
        return ret;
    }

    String unitStringText(Syntax syntax)
            throws UnwritableExpression
    {
        // It's not _completely_ obvious what the order of preference
        // should be here.  I've gone for (1) looking up the
        // functionDefinition in the given syntax, (2) looking up the
        // functionDefinition 'intrinsic' name, (3) returning the functionName.
        String name = null;
        if (functionDefinition != null) {
            // if syntax is null, default to FITS
            name = FunctionDefinitionMap.lookupFunctionName(syntax == null ? Syntax.FITS : syntax,
                                                            functionDefinition);
            if (name == null) {
                name = functionDefinition.toString();
            }
        } else {
            // if functionDefinition is null, then functionName won't be
            name = functionName;
        }
        assert name != null : String.format("Function %s/%s: can't construct unit string",
                                            functionName, functionDefinition);

        StringBuilder sb = new StringBuilder(name);
        sb.append('(').append(operand.toString(syntax)).append(')');
        return sb.toString();
    }

    String unitStringLaTeX()
            throws UnwritableExpression
    {
        StringBuilder sb = new StringBuilder();
        sb.append("\\text{\\ensuremath{");
        if (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 UnwritableExpression {
        StringBuilder sb = new StringBuilder();
        if (logFunction == null) {
            logFunction = FunctionDefinitionMap.lookupFunctionDefinition(Syntax.VOUNITS, "log");
            assert logFunction != null;
        }

        if (functionDefinition != null && functionDefinition.equals(logFunction)) {
            sb.append('[').append(operand.toString(Syntax.CDS)).append(']');
            return sb.toString();
        } else {
            throw new UnwritableExpression("CDS syntax can only express log functions, not "
                                           + (functionDefinition == null ? functionName : 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.
     * @return a function definition
     */
    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.
     * @return a string naming the function
     */
    public String getFunctionName() {
        String res;
        if (functionDefinition == null) {
            res = functionName;
        } else {
            assert functionDefinition != null;
            res = functionDefinition.name();
        }
        return res;
    }

    public String getOriginalUnitString() {
        return functionName;
    }

    @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 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) {
                cmp = u.functionDefinition == null ? -1 : functionDefinition.compareTo(u.functionDefinition);
            } else if (u.functionDefinition != null) {
                cmp = +1;
            } else {
                assert functionName != null && u.functionName != null;
                cmp = functionName.compareTo(u.functionName);
            }

            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;
    }

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

    // We override equals, so are obliged (by a warning) to override hashCode, trivially
    @Override public int hashCode() {
        return super.hashCode();
    }
}
