/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2009 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.jasperreports.olap;

import java.io.StringReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.olap.mapping.AxisPosition;
import net.sf.jasperreports.olap.mapping.DataMapping;
import net.sf.jasperreports.olap.mapping.Mapping;
import net.sf.jasperreports.olap.mapping.MappingLexer;
import net.sf.jasperreports.olap.mapping.MappingMetadata;
import net.sf.jasperreports.olap.mapping.MappingParser;
import net.sf.jasperreports.olap.mapping.MemberDepth;
import net.sf.jasperreports.olap.mapping.MemberMapping;
import net.sf.jasperreports.olap.mapping.MemberProperty;
import net.sf.jasperreports.olap.mapping.Tuple;
import net.sf.jasperreports.olap.mapping.TuplePosition;
import net.sf.jasperreports.olap.result.JROlapCell;
import net.sf.jasperreports.olap.result.JROlapHierarchy;
import net.sf.jasperreports.olap.result.JROlapHierarchyLevel;
import net.sf.jasperreports.olap.result.JROlapMember;
import net.sf.jasperreports.olap.result.JROlapMemberTuple;
import net.sf.jasperreports.olap.result.JROlapResult;
import net.sf.jasperreports.olap.result.JROlapResultAxis;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import antlr.ANTLRException;


/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 * @version $Id: JROlapDataSource.java 3678 2010-04-02 12:13:06Z shertage $
 */
public class JROlapDataSource implements JRDataSource, MappingMetadata
{
    private static final Log log = LogFactory.getLog(JROlapDataSource.class);

    protected final JROlapResult olapResult;
    protected JROlapResultAxis[] axes;
    protected final JROlapHierarchy[][] queryHierarchies;
    protected final int hierarchiesCount;

    protected Map fieldMatchers;
    protected int[][] fieldsMaxDepths;
    protected boolean[] iteratePositions;
    protected boolean iterate;

    protected boolean dataField;

    protected Map fieldValues;
    protected int[] axisPositions;
    protected boolean first;
    protected int[][] maxDepths;
        private DateFormat dateFormat = new SimpleDateFormat();
        // Mpenningroth 21-Nov-2008 added to deal with empty results.
        // Sometimes non empty can cause no results, but the init was
        // causing an error trying to locate a mapping to a tuple (there are
        // no tuples.)  This deals with that situation.
        private boolean noTuples;

    public JROlapDataSource(JRDataset dataset, JROlapResult result)
    {
        this.olapResult = result;
        axes = result.getAxes();

        queryHierarchies = new JROlapHierarchy[axes.length][];
        fieldsMaxDepths = new int[axes.length][];
        maxDepths = new int[axes.length][];
        int hCount = 0;
        noTuples = false;
        for (int i = 0; i < axes.length; i++)
        {
            noTuples = (noTuples || axes[i].getTupleCount() == 0);
            queryHierarchies[i] = axes[i].getHierarchiesOnAxis();

            hCount += queryHierarchies[i].length;
            fieldsMaxDepths[i] = new int[queryHierarchies[i].length];
            maxDepths[i] = new int[queryHierarchies[i].length];
        }
        hierarchiesCount = hCount;

        axisPositions = new int[axes.length];

        if (!noTuples)
        {
            init(dataset);
        }
    }

    public boolean next() throws JRException
    {
        boolean next;
        boolean matchMaxLevel;
        if (noTuples)
        {
            return false;
        }
        do
        {
            if (iterate)
            {
                next = nextPositions();
            }
            else
            {
                next = first;
                first = false;
            }

            if (!next)
            {
                break;
            }

            resetMaxDepths();
            for (Iterator it = fieldMatchers.entrySet().iterator(); it.hasNext();)
            {
                Map.Entry entry = (Map.Entry) it.next();
                Object fieldName = entry.getKey();
                FieldMatcher matcher = (FieldMatcher) entry.getValue();
                if (matcher.matches())
                {
                    Object value = matcher.value();
                    fieldValues.put(fieldName, value);
                }
            }

            matchMaxLevel = true;
            axes_loop:
            for (int i = 0; i < axes.length; i++)
            {
                if (iteratePositions[i])
                {
                    for (int j = 0; j < fieldsMaxDepths[i].length; j++)
                    {
                        if (maxDepths[i][j] < fieldsMaxDepths[i][j])
                        {
                            matchMaxLevel = false;
                            break axes_loop;
                        }
                    }
                }
            }
        }
        while (!matchMaxLevel);

        return next;
    }


    private void resetMaxDepths()
    {
        for (int i = 0; i < axes.length; ++i)
        {
            if (iteratePositions[i])
            {
                for (int j = 0; j < maxDepths[i].length; j++)
                {
                    maxDepths[i][j] = 0;
                }
            }
        }
    }

    protected boolean nextPositions()
    {
        boolean next;
        int i = 0;
        for (; i < axes.length; ++i)
        {
            if (iteratePositions[i])
            {
                ++axisPositions[i];
                if (axisPositions[i] >= axes[i].getTupleCount())
                {
                    axisPositions[i] = 0;
                }
                else
                {
                    break;
                }
            }
        }

        next = i < axes.length;
        return next;
    }

    /**
     * Convert the value of the data type of the Field
     * @param jrField the Field whose type has to be converted
     * @return value of field in the requested type
     *
     */
    public Object getFieldValue(JRField jrField) throws JRException {
        Class valueClass = jrField.getValueClass();
        Object value = fieldValues.get(jrField.getName());

        try {
            /*
             * Everything in the result is a string, apart from Member
             */
            if (valueClass.equals(mondrian.olap.Member.class)) {
                if (!(value instanceof mondrian.olap.Member)) {
                    throw new JRException("Field '" + jrField.getName() + "' is of class '"
                        + value.getClass()
                        + "' and can not be converted to class " + valueClass.getName());
                }

                return value;
            }

            /*
             * Convert the rest from String
             */
            String fieldValue = (String) value;

            if (fieldValue == null) 
            {	
            	return null;
            }
            if (Number.class.isAssignableFrom(valueClass)){
                fieldValue = fieldValue.trim();
            }
            if (fieldValue.length() == 0){
               fieldValue = "0";
            }

            if (valueClass.equals(String.class)) {
                return fieldValue;
            } else if (valueClass.equals(Boolean.class)) {
                return fieldValue.equalsIgnoreCase("true") ? Boolean.TRUE : Boolean.FALSE;
            } else if (valueClass.equals(Byte.class)) {
                return new Byte(fieldValue);
            } else if (valueClass.equals(Integer.class)) {
                return Integer.valueOf(fieldValue);
            } else if (valueClass.equals(Long.class)) {
                return new Long(fieldValue);
            } else if (valueClass.equals(Short.class)) {
                return new Short(fieldValue);
            } else if (valueClass.equals(Double.class)) {
                return new Double(fieldValue);
            } else if (valueClass.equals(Float.class)) {
                return new Float(fieldValue);
            } else if (valueClass.equals(java.math.BigDecimal.class)) {
                return new java.math.BigDecimal(fieldValue);
            } else if (valueClass.equals(java.util.Date.class)) {
                return dateFormat.parse(fieldValue);
            } else if (valueClass.equals(java.sql.Timestamp.class)) {
                return new java.sql.Timestamp(dateFormat.parse(fieldValue).getTime());
            } else if (valueClass.equals(java.sql.Time.class)) {
                return new java.sql.Time(dateFormat.parse(fieldValue).getTime());
            } else if (valueClass.equals(java.lang.Number.class)) {
                return new Double(fieldValue);
            } else {
                throw new JRException("Field '" + jrField.getName() + "', string value '" + fieldValue + "' is of class '"
                + fieldValues.get(jrField.getName()).getClass()
                + "' and can not be converted to class " + valueClass.getName());
            }
        } catch (Exception e) {
            throw new JRException("Unable to get value for field '" + jrField.getName() + "' of class '" + valueClass.getName() + "'", e);
        }
    }

    private void init(JRDataset dataset)
    {
        iteratePositions = new boolean[axes.length];

        fieldMatchers = new HashMap();

        dataField = false;
        JRField[] fields = dataset.getFields();
        if (fields != null)
        {
            for (int i = 0; i < fields.length; i++)
            {
                JRField field = fields[i];
                String fieldMapping = getFieldMapping(field);

                MappingLexer lexer = new MappingLexer(new StringReader(fieldMapping));
                MappingParser parser = new MappingParser(lexer);
                parser.setMappingMetadata(this);
                Mapping mapping;
                try
                {
                    mapping = parser.mapping();
                }
                catch (ANTLRException e)
                {
                    log.error("Error parsing field mapping", e);
                    throw new JRRuntimeException(e);
                }

                if (mapping == null)
                {
                    throw new JRRuntimeException("Invalid field mapping \"" + fieldMapping + "\".");
                }

                processMappingMembers(mapping);

                FieldMatcher fieldMatcher = createFieldMatcher(mapping);
                fieldMatchers.put(field.getName(), fieldMatcher);
            }
        }

        if (!dataField)
        {
            Arrays.fill(iteratePositions, true);
        }

        initIterate();
    }

    private void processMappingMembers(Mapping mapping)
    {
        for (Iterator it = mapping.memberMappings(); it.hasNext();)
        {
            net.sf.jasperreports.olap.mapping.Member member = (net.sf.jasperreports.olap.mapping.Member) it.next();
            processMemberInfo(member);
        }
    }

    private FieldMatcher createFieldMatcher(Mapping mapping)
    {
        FieldMatcher fieldMatcher;
        if (mapping instanceof MemberMapping)
        {
            fieldMatcher = new MemberFieldMatcher((MemberMapping) mapping);
        }
        else if (mapping instanceof DataMapping)
        {
            dataField = true;
            fieldMatcher = new DataFieldMatcher((DataMapping) mapping);
        }
        else
        {
            throw new JRRuntimeException("internal error");
        }

        return fieldMatcher;
    }

    protected String getFieldMapping(JRField field)
    {
        return field.getDescription();
    }

    private void initIterate()
    {
        int firstPos = 0;
        while (firstPos < axes.length && !iteratePositions[firstPos])
        {
            ++firstPos;
        }

        if (firstPos < axes.length)
        {
            iterate = true;
            axisPositions[firstPos] = -1;
        }
        else
        {
            iterate = false;
            first = true;
        }

        fieldValues = new HashMap();
    }


    protected void processMemberInfo(net.sf.jasperreports.olap.mapping.Member member)
    {
        MemberDepth memberDepth = member.getDepth();
        if (memberDepth != null)
        {
            int depth = memberDepth.getDepth();
            int axis = member.getAxis().getIdx();
            int idx = member.getPosition().getIdx();

            if (depth > fieldsMaxDepths[axis][idx])
            {
                fieldsMaxDepths[axis][idx] = depth;
            }
        }
    }


    public int getDimensionIndex(net.sf.jasperreports.olap.mapping.Axis axis, String dimension)
    {
        JROlapHierarchy[] hierarchies = axes[axis.getIdx()].getHierarchiesOnAxis();
        int dimensionIndex = -1;
        for (int i = 0; i < hierarchies.length; i++)
        {
            JROlapHierarchy hierarchy = hierarchies[i];
            if (dimension.equals(hierarchy.getDimensionName()))
            {
                dimensionIndex = i;
            }
        }
        // MPenningroth 21-April-2009 deal with case when dimension is <dimension>.<hierarchy> form
        if (dimensionIndex == -1 && dimension.indexOf('.')!= -1 ) {
            String hierName = "[" + dimension + "]";
            for (int i = 0; i < hierarchies.length; i++)
            {
                JROlapHierarchy hierarchy = hierarchies[i];
                if (hierName.equals(hierarchy.getHierarchyUniqueName()))
                {
                    dimensionIndex = i;
                }
            }
        }

        if (dimensionIndex == -1)
        {
            throw new JRRuntimeException("Could not find dimension \"" + dimension + "\" on axis " + axis.getIdx() + ".");
        }

        return dimensionIndex;
    }

    public int getLevelDepth(TuplePosition pos, String levelName)
    {
        JROlapHierarchy hierarchy = axes[pos.getAxis().getIdx()].getHierarchiesOnAxis()[pos.getIdx()];
        JROlapHierarchyLevel[] levels = hierarchy.getLevels();
        int levelIndex = -1;
        for (int i = 0; i < levels.length; i++)
        {
            JROlapHierarchyLevel level = levels[i];
            if (level != null && level.getName().equals(levelName))
            {
                levelIndex = level.getDepth();
                break;
            }
        }

        if (levelIndex == -1)
        {
            throw new JRRuntimeException("Could not find level \"" + levelName
                    + "\" on hierarchy #" + pos.getIdx() + " (dimension " + hierarchy.getDimensionName()
                    + ") on axis #" + pos.getAxis().getIdx());
        }

        return levelIndex;
    }


    protected void setMatchMemberDepth(net.sf.jasperreports.olap.mapping.Member memberInfo, JROlapMember member)
    {
        int memberDepth = member.getDepth();
        int axis = memberInfo.getAxis().getIdx();
        int pos = memberInfo.getPosition().getIdx();
        if (maxDepths[axis][pos] < memberDepth)
        {
            maxDepths[axis][pos] = memberDepth;
        }
    }


    protected abstract class FieldMatcher
    {
        public abstract boolean matches();

        public abstract Object value();

        public final JROlapMember member(net.sf.jasperreports.olap.mapping.Member memberInfo, int[] positions)
        {
            int axisIdx = memberInfo.getAxis().getIdx();
            JROlapResultAxis axis = axes[axisIdx];
            JROlapMemberTuple tuple = axis.getTuple(positions[axisIdx]);
            JROlapMember[] members = tuple.getMembers();
            int pos = memberInfo.getPosition().getIdx();
            return members[pos];
        }
    }

    protected class MemberFieldMatcher extends FieldMatcher
    {
        final net.sf.jasperreports.olap.mapping.Member memberInfo;
        final MemberProperty property;
        JROlapMember member;

        MemberFieldMatcher(MemberMapping mapping)
        {
            this.memberInfo = mapping.getMember();
            this.property = mapping.getProperty();
        }

        public boolean matches()
        {
            member = member(memberInfo, axisPositions);
            setMatchMemberDepth(memberInfo, member);
            member = memberInfo.ancestorMatch(member);
            return member != null;
        }

        public Object value()
        {
            Object value;

            if (memberInfo.getDepth() == null)
            {
                // The actual member object of the given dimension
                return member.getMondrianMember();
            }
            else if (property != null)
            {
                // member property value
                value = member.getPropertyValue(property.getName());
            }
            else
            {
                // Level name
                value = member.getName();
            }
            return value.toString();
        }
    }


    protected class DataFieldMatcher extends FieldMatcher
    {
        private final boolean formatted;
        private final int[] dataPositions;
        private final net.sf.jasperreports.olap.mapping.Member[] members;
        private int[] positions;

        public DataFieldMatcher(DataMapping mapping)
        {
            this.formatted = mapping.isFormatted();

            List mappingPositions = mapping.getPositions();
            if (mappingPositions == null)
            {
                this.dataPositions = null;
                positions = axisPositions;
            }
            else
            {
                if (mappingPositions.size() != axes.length)
                {
                    throw new JRRuntimeException("Incorrect data mapping: the number of positions doesn't match the number of axes.");
                }

                this.dataPositions = new int[axes.length];
                int c = 0;
                for (Iterator iter = mappingPositions.iterator(); iter.hasNext(); ++c)
                {
                    AxisPosition position = (AxisPosition) iter.next();
                    int pos;
                    if (position.isSpecified())
                    {
                        pos = position.getPosition();
                    }
                    else
                    {
                        pos = -1;
                        iteratePositions[c] = true;
                    }
                    dataPositions[c] = pos;
                }
            }

            List filter = mapping.getFilter();
            if (filter == null || filter.isEmpty())
            {
                this.members = null;
            }
            else
            {
                this.members = new net.sf.jasperreports.olap.mapping.Member[filter.size()];
                filter.toArray(this.members);
            }
        }

        public boolean matches()
        {
            if (dataPositions != null)
            {
                setPositions();
            }

            boolean matches = true;
            if (members != null)
            {
                for (int i = 0; i < members.length; i++)
                {
                    net.sf.jasperreports.olap.mapping.Member memberInfo = members[i];
                    JROlapMember member = member(memberInfo, positions);
                    setMatchMemberDepth(memberInfo, member);
                    if (!memberInfo.matches(member))
                    {
                        matches = false;
                        break;
                    }
                }
            }

            return matches;
        }

        public Object value()
        {
            JROlapCell cell = olapResult.getCell(positions);

            if (cell != null && cell.isError())
            {
                throw new JRRuntimeException("OLAP cell calculation returned error.");
            }

            Object value;
            if (cell == null || cell.isNull())
            {
                value = null;
            }
            else if (formatted)
            {
                value = cell.getFormattedValue();
            }
            else
            {
                value = cell.getValue().toString();
            }

            return value;
        }

        void setPositions()
        {
            positions = new int[axes.length];
            for (int i = 0; i < axes.length; i++)
            {
                int dataPosition = dataPositions[i];
                positions[i] = dataPosition == -1 ? axisPositions[i] : dataPosition;
            }
        }
    }


    public int getTuplePosition(int axisIndex, Tuple tuple)
    {
        if (axisIndex > axes.length)
        {
            throw new JRRuntimeException("OLAP result doesn't contain Axis(" + axisIndex + ").");
        }

        String[] memberUniqueNames = tuple.getMemberUniqueNames();
        JROlapResultAxis axis = axes[axisIndex];
        int tupleCount = axis.getTupleCount();

        int pos = -1;
        for (int i = 0; i < tupleCount; i++)
        {
            JROlapMemberTuple memberTuple = axis.getTuple(i);
            JROlapMember[] resMembers = memberTuple.getMembers();
            if (resMembers.length == memberUniqueNames.length)
            {
                boolean eq = true;
                for (int j = 0; eq && j < resMembers.length; ++j)
                {
                    if (!memberUniqueNames[j].equals(resMembers[j].getUniqueName()))
                    {
                        eq = false;
                    }
                }

                if (eq)
                {
                    pos = i;
                    break;
                }
            }
        }

        if (pos == -1)
        {
            StringBuffer sb = new StringBuffer();
            sb.append('(');
            for (int i = 0; i < memberUniqueNames.length; i++)
            {
                if (i > 0)
                {
                    sb.append(',');
                }
                sb.append(memberUniqueNames[i]);
            }
            throw new JRRuntimeException("No such tuple " + sb + " on axis " + axisIndex + ".");
        }

        return pos;
    }
}
