/*
* This software is subject to the terms of the Eclipse Public License v1.0
* Agreement, available at the following URL:
* http://www.eclipse.org/legal/epl-v10.html.
* You must accept the terms of that agreement to use this software.
*
* Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
*/

package mondrian.rolap;

import mondrian.calc.Calc;
import mondrian.calc.DummyExp;
import mondrian.calc.impl.GenericCalc;
import mondrian.mdx.ResolvedFunCall;
import mondrian.olap.*;
import mondrian.olap.type.ScalarType;

import org.olap4j.AllocationPolicy;
import org.olap4j.Scenario;

import java.util.ArrayList;
import java.util.List;

/**
 * Implementation of {@link org.olap4j.Scenario}.
 *
 * @author jhyde
 * @since 24 April, 2009
 */
public final class ScenarioImpl implements Scenario {

    private final int id;

    private final List<WritebackCell> writebackCells =
        new ArrayList<WritebackCell>();

    private RolapMember member;

    private static int nextId;

    /**
     * Creates a ScenarioImpl.
     */
    public ScenarioImpl() {
        id = nextId++;
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof ScenarioImpl
            && id == ((ScenarioImpl) obj).id;
    }

    @Override
    public String toString() {
        return "scenario #" + id;
    }

    /**
     * Sets the value of a cell.
     *
     * @param connection Connection (not currently used)
     * @param members Coordinates of cell
     * @param newValue New value
     * @param currentValue Current value
     * @param allocationPolicy Allocation policy
     * @param allocationArgs Additional arguments of allocation policy
     */
    public void setCellValue(
        Connection connection,
        List<RolapMember> members,
        double newValue,
        double currentValue,
        AllocationPolicy allocationPolicy,
        Object[] allocationArgs)
    {
        Util.discard(connection); // for future use
        assert allocationPolicy != null;
        assert allocationArgs != null;
        switch (allocationPolicy) {
        case EQUAL_ALLOCATION:
        case EQUAL_INCREMENT:
            if (allocationArgs.length != 0) {
                throw Util.newError(
                    "Allocation policy " + allocationPolicy
                    + " takes 0 arguments; " + allocationArgs.length
                    + " were supplied");
            }
            break;
        default:
            throw Util.newError(
                "Allocation policy " + allocationPolicy + " is not supported");
        }

        // Compute the set of columns which are constrained by the cell's
        // coordinates.
        //
        // NOTE: This code is very similar to code in
        // RolapAggregationManager.makeCellRequest. Consider creating a
        // CellRequest then mining it. It will work better in the presence of
        // calculated members, compound members, parent-child hierarchies,
        // hierarchies whose default member is not the 'all' member, and so
        // forth.
        final RolapStoredMeasure measure = (RolapStoredMeasure) members.get(0);
        final RolapCube baseCube = measure.getCube();
        final RolapStar.Measure starMeasure =
            (RolapStar.Measure) measure.getStarMeasure();
        assert starMeasure != null;
        int starColumnCount = starMeasure.getStar().getColumnCount();
        final BitKey constrainedColumnsBitKey =
            BitKey.Factory.makeBitKey(starColumnCount);
        Object[] keyValues = new Object[starColumnCount];
        for (int i = 1; i < members.size(); i++) {
            Member member = members.get(i);
            for (RolapCubeMember m = (RolapCubeMember) member;
                m != null && !m.isAll();
                m = m.getParentMember())
            {
                final RolapCubeLevel level = m.getLevel();
                RolapStar.Column column = level.getBaseStarKeyColumn(baseCube);
                if (column != null) {
                    final int bitPos = column.getBitPosition();
                    keyValues[bitPos] = m.getKey();
                    constrainedColumnsBitKey.set(bitPos);
                }
                if (level.areMembersUnique()) {
                    break;
                }
            }
        }

        // Squish the values down. We want the compactKeyValues[i] to correspond
        // to the i'th set bit in the key. This is the same format used by
        // CellRequest.
        Object[] compactKeyValues =
            new Object[constrainedColumnsBitKey.cardinality()];
        int k = 0;
        for (int bitPos : constrainedColumnsBitKey) {
            compactKeyValues[k++] = keyValues[bitPos];
        }

        // Record the override.
        //
        // TODO: add a mechanism for persisting the overrides to a file.
        //
        // FIXME: make thread-safe
        writebackCells.add(
            new WritebackCell(
                baseCube,
                new ArrayList<RolapMember>(members),
                constrainedColumnsBitKey,
                compactKeyValues,
                newValue,
                currentValue,
                allocationPolicy));
    }

    public String getId() {
        return Integer.toString(id);
    }

    /**
     * Returns the scenario inside a calculated member in the scenario
     * dimension. For example, applied to [Scenario].[1], returns the Scenario
     * object representing scenario #1.
     *
     * @param member Wrapper member
     * @return Wrapped scenario
     */
    static Scenario forMember(final RolapMember member) {
        if (isScenario(member.getHierarchy())) {
            final Formula formula = ((RolapCalculatedMember) member)
                .getFormula();
            final ResolvedFunCall resolvedFunCall =
                (ResolvedFunCall) formula.getExpression();
            final Calc calc = resolvedFunCall.getFunDef()
                .compileCall(null, null);
            return ((ScenarioCalc) calc).getScenario();
        } else {
            return null;
        }
    }

    /**
     * Registers this Scenario with a Schema, creating a calulated member
     * [Scenario].[{id}] for each cube that has writeback enabled. (Currently
     * a cube has writeback enabled iff it has a dimension called "Scenario".)
     *
     * @param schema Schema
     */
    void register(RolapSchema schema) {
        // Add a value to the [Scenario] dimension of every cube that has
        // writeback enabled.
        for (RolapCube cube : schema.getCubeList()) {
            for (RolapHierarchy hierarchy : cube.getHierarchies()) {
                if (isScenario(hierarchy)) {
                    member =
                        cube.createCalculatedMember(
                            hierarchy,
                            getId() + "",
                            new ScenarioCalc(this));
                    assert member != null;
                }
            }
        }
    }

    /**
     * Returns whether a hierarchy is the [Scenario] hierarchy.
     *
     * <p>TODO: use a flag
     *
     * @param hierarchy Hierarchy
     * @return Whether hierarchy is the scenario hierarchy
     */
    public static boolean isScenario(Hierarchy hierarchy) {
        return hierarchy.getName().equals("Scenario");
    }

    /**
     * Returns the number of atomic cells that contribute to the current
     * cell.
     *
     * @param evaluator Evaluator
     * @return Number of atomic cells in the current cell
     */
    private static double evaluateAtomicCellCount(RolapEvaluator evaluator) {
        final int savepoint = evaluator.savepoint();
        try {
            evaluator.setContext(
                evaluator.getCube().getAtomicCellCountMeasure());
            final Object o = evaluator.evaluateCurrent();
            return ((Number) o).doubleValue();
        } finally {
            evaluator.restore(savepoint);
        }
    }

    /**
     * Computes the number of atomic cells in a cell identified by a list
     * of members.
     *
     * <p>The method may be expensive. If the value is not in the cache,
     * computes it immediately using a database access. It uses an aggregate
     * table if applicable, and puts the value into the cache.
     *
     * <p>Compare with {@link #evaluateAtomicCellCount(RolapEvaluator)}, which
     * gets the value from the cache but may lie (and generate a cache miss) if
     * the value is not present.
     *
     * @param cube Cube
     * @param memberList Coordinate members of cell
     * @return Number of atomic cells in cell
     */
    private static double computeAtomicCellCount(
        RolapCube cube, List<RolapMember> memberList)
    {
        // Implementation generates and executes a recursive MDX query. This
        // may not be the most efficient implementation, but achieves the
        // design goals of (a) immediacy, (b) cache use, (c) aggregate table
        // use.
        final StringBuilder buf = new StringBuilder();
        buf.append("select from ");
        buf.append(cube.getUniqueName());
        int k = 0;
        for (Member member : memberList) {
            if (member.isMeasure()) {
                member = cube.factCountMeasure;
                assert member != null
                    : "fact count measure is required for writeback cubes";
            }
            if (!member.equals(member.getHierarchy().getDefaultMember())) {
                if (k++ > 0) {
                    buf.append(", ");
                } else {
                    buf.append(" where (");
                }
                buf.append(member.getUniqueName());
            }
        }
        if (k > 0) {
            buf.append(")");
        }
        final String mdx = buf.toString();
        final RolapConnection connection =
            cube.getSchema().getInternalConnection();
        final Query query = connection.parseQuery(mdx);
        final Result result = connection.execute(query);
        final Object o = result.getCell(new int[0]).getValue();
        return o instanceof Number
            ? ((Number) o).doubleValue()
            : 0d;
    }

    /**
     * Returns the member of the [Scenario] dimension that represents this
     * scenario. Including that member in the slicer will automatically use
     * this scenario.
     *
     * <p>The result is not null, provided that {@link #register(RolapSchema)}
     * has been called.
     *
     * @return Scenario member
     */
    public RolapMember getMember() {
        return member;
    }

    /**
     * Created by a call to
     * {@link org.olap4j.Cell#setValue(Object, org.olap4j.AllocationPolicy, Object...)},
     * records that a cell's value has been changed.
     *
     * <p>From this, other cell values can be modified as they are read into
     * cache. Only the cells specifically modified by the client have a
     * {@code CellValueOverride}.
     *
     * <p>In future, a {@link ScenarioImpl} could be persisted by
     * serializing all {@code WritebackCell}s to a file.
     */
    private static class WritebackCell {
        private final double newValue;
        private final double currentValue;
        private final AllocationPolicy allocationPolicy;
        private Member[] membersByOrdinal;
        private final double atomicCellCount;

        /**
         * Creates a WritebackCell.
         *
         * @param cube Cube
         * @param members Members that form context
         * @param constrainedColumnsBitKey Bitmap of columns which have values
         * @param keyValues List of values, by bit position
         * @param newValue New value
         * @param currentValue Current value
         * @param allocationPolicy Allocation policy
         */
        WritebackCell(
            RolapCube cube,
            List<RolapMember> members,
            BitKey constrainedColumnsBitKey,
            Object[] keyValues,
            double newValue,
            double currentValue,
            AllocationPolicy allocationPolicy)
        {
            assert keyValues.length == constrainedColumnsBitKey.cardinality();
            Util.discard(cube); // not used currently
            Util.discard(constrainedColumnsBitKey); // not used currently
            Util.discard(keyValues); // not used currently
            this.newValue = newValue;
            this.currentValue = currentValue;
            this.allocationPolicy = allocationPolicy;
            this.atomicCellCount = computeAtomicCellCount(cube, members);

            // Build the array of members by ordinal. If a member is not
            // specified for a particular dimension, use the 'all' member (not
            // necessarily the same as the default member).
            final List<RolapHierarchy> hierarchyList = cube.getHierarchies();
            this.membersByOrdinal = new Member[hierarchyList.size()];
            for (int i = 0; i < membersByOrdinal.length; i++) {
                membersByOrdinal[i] = hierarchyList.get(i).getDefaultMember();
            }
            for (RolapMember member : members) {
                final RolapHierarchy hierarchy = member.getHierarchy();
                if (isScenario(hierarchy)) {
                    assert member.isAll();
                }
                // REVIEW The following works because Measures is the only
                // dimension whose members do not belong to RolapCubeDimension,
                // just a regular RolapDimension, but has ordinal 0.
                final int ordinal = hierarchy.getOrdinalInCube();
                membersByOrdinal[ordinal] = member;
            }
        }

        /**
         * Returns the amount by which the cell value has increased with this
         * override.
         *
         * @return Amount by which value has increased
         */
        public double getOffset() {
            return newValue - currentValue;
        }

        /**
         * Returns the position of this writeback cell relative to another
         * co-ordinate.
         *
         * <p>Assumes that {@code members} contains an entry for each dimension
         * in the cube.
         *
         * @param members Co-ordinates of another cell
         * @return Relation of this writeback cell to other co-ordinate, never
         * null
         */
        CellRelation getRelationTo(Member[] members) {
            int aboveCount = 0;
            int belowCount = 0;
            for (int i = 0; i < members.length; i++) {
                Member thatMember = members[i];
                Member thisMember = membersByOrdinal[i];
                // FIXME: isChildOrEqualTo is very inefficient. It should use
                // level depth as a guideline, at least.
                if (thatMember.isChildOrEqualTo(thisMember)) {
                    if (thatMember.equals(thisMember)) {
                        // thisMember equals member
                    } else {
                        // thisMember is ancestor of member
                        ++aboveCount;
                        if (belowCount > 0) {
                            return CellRelation.NONE;
                        }
                    }
                } else if (thisMember.isChildOrEqualTo(thatMember)) {
                    // thisMember is descendant of member
                    ++belowCount;
                    if (aboveCount > 0) {
                        return CellRelation.NONE;
                    }
                } else {
                    return CellRelation.NONE;
                }
            }
            assert aboveCount == 0 || belowCount == 0;
            if (aboveCount > 0) {
                return CellRelation.ABOVE;
            } else if (belowCount > 0) {
                return CellRelation.BELOW;
            } else {
                return CellRelation.EQUAL;
            }
        }
    }

    /**
     * Decribes the relationship between two cells.
     */
    enum CellRelation {
        ABOVE,
        EQUAL,
        BELOW,
        NONE
    }

    /**
     * Compiled expression to implement a [Scenario].[{name}] calculated member.
     *
     * <p>When evaluated, replaces the value of a cell with the value overridden
     * by a writeback value, per
     * {@link org.olap4j.Cell#setValue(Object, org.olap4j.AllocationPolicy, Object...)},
     * and modifies the values of ancestors or descendants of such cells
     * according to the allocation policy.
     */
    private static class ScenarioCalc extends GenericCalc {
        private final ScenarioImpl scenario;

        /**
         * Creates a ScenarioCalc.
         *
         * @param scenario Scenario whose writeback values should be substituted
         * for the values stored in the database.
         */
        public ScenarioCalc(ScenarioImpl scenario) {
            super(new DummyExp(new ScalarType()));
            this.scenario = scenario;
        }

        /**
         * Returns the Scenario this writeback cell belongs to.
         *
         * @return Scenario, never null
         */
        private Scenario getScenario() {
            return scenario;
        }

        public Object evaluate(Evaluator evaluator) {
            // Evaluate current member in the given scenario by expanding in
            // terms of the writeback cells.

            // First, evaluate in the null scenario.
            final Member defaultMember =
                scenario.member.getHierarchy().getDefaultMember();
            final int savepoint = evaluator.savepoint();
            try {
                evaluator.setContext(defaultMember);
                final Object o = evaluator.evaluateCurrent();
                double d =
                    o instanceof Number
                        ? ((Number) o).doubleValue()
                        : 0d;

                // Look for writeback cells which are equal to, ancestors of,
                // or descendants of, the current cell. Modify the value
                // accordingly.
                //
                // It is possible that the value is modified by several
                // writebacks. If so, order is important.
                int changeCount = 0;
                for (ScenarioImpl.WritebackCell writebackCell
                    : scenario.writebackCells)
                {
                    CellRelation relation =
                        writebackCell.getRelationTo(evaluator.getMembers());
                    switch (relation) {
                    case ABOVE:
                        // This cell is below the writeback cell. Value is
                        // determined by allocation policy.
                        double atomicCellCount =
                        evaluateAtomicCellCount((RolapEvaluator) evaluator);
                        if (atomicCellCount == 0d) {
                            // Sometimes the value comes back zero if the cache
                            // is not ready. Switch to 1, which at least does
                            // not give divide-by-zero. We will be invoked again
                            // for the correct answer when the cache has been
                            // populated.
                            atomicCellCount = 1d;
                        }
                        switch (writebackCell.allocationPolicy) {
                        case EQUAL_ALLOCATION:
                            d = writebackCell.newValue
                            * atomicCellCount
                            / writebackCell.atomicCellCount;
                            break;
                        case EQUAL_INCREMENT:
                            d += writebackCell.getOffset()
                            * atomicCellCount
                            / writebackCell.atomicCellCount;
                            break;
                        default:
                            throw Util.unexpected(
                                writebackCell.allocationPolicy);
                        }
                        ++changeCount;
                        break;
                    case EQUAL:
                        // This cell is the writeback cell. Value is the value
                        // written back.
                        d = writebackCell.newValue;
                        ++changeCount;
                        break;
                    case BELOW:
                        // This cell is above the writeback cell. Value is the
                        // current value plus the change in the writeback cell.
                        d += writebackCell.getOffset();
                        ++changeCount;
                        break;
                    case NONE:
                        // Writeback cell is unrelated. It has no effect on
                        // cell's value.
                        break;
                    default:
                        throw Util.unexpected(relation);
                    }
                }
                // Don't create a new object if value has not changed.
                if (changeCount == 0) {
                    return o;
                } else {
                    return d;
                }
            } finally {
                evaluator.restore(savepoint);
            }
        }
    }
}

// End ScenarioImpl.java
