/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * 
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org 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 version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/
package com.sun.star.report.pentaho;


import com.sun.star.lang.XServiceInfo;
import com.sun.star.lib.uno.helper.ComponentBase;
import com.sun.star.lib.uno.helper.PropertySetMixin;
import com.sun.star.sheet.FormulaLanguage;
import com.sun.star.sheet.FormulaMapGroup;
import com.sun.star.sheet.FormulaMapGroupSpecialOffset;
import com.sun.star.sheet.FormulaOpCodeMapEntry;
import com.sun.star.sheet.FormulaToken;
import com.sun.star.sheet.XFormulaOpCodeMapper;
import com.sun.star.uno.Any;
import com.sun.star.uno.Exception;
import com.sun.star.uno.Type;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;

import java.io.StringReader;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.formula.DefaultFormulaContext;
import org.pentaho.reporting.libraries.formula.function.FunctionRegistry;
import org.pentaho.reporting.libraries.formula.parser.FormulaParser;
import org.pentaho.reporting.libraries.formula.parser.GeneratedFormulaParserConstants;
import org.pentaho.reporting.libraries.formula.parser.GeneratedFormulaParserTokenManager;
import org.pentaho.reporting.libraries.formula.parser.JavaCharStream;
import org.pentaho.reporting.libraries.formula.parser.ParseException;
import org.pentaho.reporting.libraries.formula.parser.Token;
import org.pentaho.reporting.libraries.formula.parser.TokenMgrError;


public final class SOFormulaParser extends ComponentBase
        implements com.sun.star.report.meta.XFormulaParser, XServiceInfo
{

    public static final int SEPARATORS = 0;
    public static final int ARRAY_SEPARATORS = 1;
    public static final int UNARY_OPERATORS = 2;
    public static final int BINARY_OPERATORS = 3;
    public static final int FUNCTIONS = 4;
    private final XComponentContext m_xContext;
    private final PropertySetMixin m_prophlp;
    private static final String __serviceName = "com.sun.star.report.meta.FormulaParser";
    private static final String OPERATORS = "org.pentaho.reporting.libraries.formula.operators.";
    // attributes
    final private List m_OpCodeMap = new ArrayList();
    private XFormulaOpCodeMapper formulaOpCodeMapper = null;
    private final Map parserAllOpCodes = new HashMap();
    private final Map parserNames = new HashMap();
    private final Map[] groupOpCodes = new HashMap[5];
    private final List specialOpCodes = new ArrayList();

    public List getSpecialOpCodes()
    {
        return specialOpCodes;
    }
    private int ownTokenCounter = 1000;
    private final FormulaOpCodeMapEntry opCodePush;
    private final FormulaParser parser;

    public SOFormulaParser(final XComponentContext context)
    {

        m_xContext = context;
        final ClassLoader cl = java.lang.Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());

        parser = new FormulaParser();
        try
        {
            final XFormulaOpCodeMapper mapper = (XFormulaOpCodeMapper) UnoRuntime.queryInterface(XFormulaOpCodeMapper.class, m_xContext.getServiceManager().createInstanceWithContext("simple.formula.FormulaOpCodeMapperObj", m_xContext));
            FormulaOpCodeMapEntry[] opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.FUNCTIONS);
            final DefaultFormulaContext defaultContext = new DefaultFormulaContext();
            final FunctionRegistry functionRegistry = defaultContext.getFunctionRegistry();

            String[] names = functionRegistry.getFunctionNames();
            addOpCodes(names, opCodes, FUNCTIONS);
            names = getOperators(defaultContext, OPERATORS);
            opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.UNARY_OPERATORS);
            addOpCodes(names, opCodes, UNARY_OPERATORS);
            opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.BINARY_OPERATORS);
            addOpCodes(names, opCodes, BINARY_OPERATORS);

            names = GeneratedFormulaParserConstants.tokenImage.clone();
            for (int i = 0; i < names.length; i++)
            {
                final String token = names[i];
                if (token != null && token.length() > 0 && token.charAt(0) == '"')
                {
                    names[i] = token.substring(1, token.length() - 1);
                }
            }
            opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.SEPARATORS);
            addOpCodes(names, opCodes, SEPARATORS, false);

            opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.ARRAY_SEPARATORS);
            addOpCodes(names, opCodes, ARRAY_SEPARATORS, false);

            opCodes = mapper.getAvailableMappings(FormulaLanguage.ODFF, FormulaMapGroup.SPECIAL);

            for (int i = 0; i < opCodes.length; i++)
            {
                final FormulaOpCodeMapEntry opCode = opCodes[i];
                parserAllOpCodes.put(opCode.Token.OpCode, opCode);
                specialOpCodes.add(opCode);
            }
            // addOpCodes(names, opCodes,SPECIAL,false);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
        opCodePush = (FormulaOpCodeMapEntry) specialOpCodes.get(FormulaMapGroupSpecialOffset.PUSH);
        Thread.currentThread().setContextClassLoader(cl);
        // use the last parameter of the PropertySetMixin constructor
        // for your optional attributes if necessary. See the documentation
        // of the PropertySetMixin helper for further information.
        // Ensure that your attributes are initialized correctly!
        m_prophlp = new PropertySetMixin(m_xContext, this,
                new Type(com.sun.star.report.meta.XFormulaParser.class), null);
    }

    // com.sun.star.sheet.XFormulaParser:
    public com.sun.star.sheet.FormulaToken[] parseFormula(String aFormula, com.sun.star.table.CellAddress aReferencePos)
    {
        final ArrayList tokens = new ArrayList();
        if (!"=".equals(aFormula))
        {
            String formula;
            if (aFormula.charAt(0) == '=')
            {
                formula = aFormula.substring(1);
            }
            else
            {
                formula = aFormula;
            }
            final ArrayList images = new ArrayList();
            try
            {
                int brackets = 0;
                final GeneratedFormulaParserTokenManager tokenParser = new GeneratedFormulaParserTokenManager(new JavaCharStream(new StringReader(formula), 1, 1));
                Token token = tokenParser.getNextToken();
                while (token.kind != GeneratedFormulaParserConstants.EOF)
                {
                    final FormulaToken formulaToken;
                    images.add(token.image);
                    final String upper = token.image.toUpperCase();
                    if (parserNames.containsKey(upper))
                    {
                        if ("(".equals(token.image))
                        {
                            brackets++;
                        }
                        else if (")".equals(token.image))
                        {
                            --brackets;
                        }
                        final FormulaOpCodeMapEntry opCode = (FormulaOpCodeMapEntry) parserNames.get(upper);
                        formulaToken = opCode.Token;
                    }
                    else if (token.kind == GeneratedFormulaParserConstants.WHITESPACE)
                    {
                        final FormulaOpCodeMapEntry opCode = (FormulaOpCodeMapEntry) specialOpCodes.get(FormulaMapGroupSpecialOffset.SPACES);
                        formulaToken = opCode.Token;
                    }
                    else
                    {
                        formulaToken = new FormulaToken();
                        formulaToken.OpCode = opCodePush.Token.OpCode;
                        formulaToken.Data = new Any(Type.STRING, token.image);
                    }

                    tokens.add(formulaToken);
                    token = tokenParser.getNextToken();
                }
                if (brackets > 0)
                {
                    final FormulaOpCodeMapEntry opCode = (FormulaOpCodeMapEntry) parserNames.get(")");
                    while (brackets-- != 0)
                    {
                        formula = formula.concat(")");
                        images.add(")");
                        tokens.add(opCode.Token);
                    }

                }

                parser.parse(formula);
            }
            catch (ParseException ex)
            {
                boolean found = false;
                // error occured so all token must be bad
                for (int i = 0; i < tokens.size(); i++)
                {
                    if (!found && ex.currentToken != null && images.get(i).equals(ex.currentToken.image))
                    {
                        found = true;
                    }
                    if (found)
                    {
                        final FormulaToken dest = new FormulaToken();
                        dest.OpCode = ((FormulaOpCodeMapEntry) specialOpCodes.get(FormulaMapGroupSpecialOffset.BAD)).Token.OpCode;
                        dest.Data = new Any(Type.STRING, images.get(i));
                        tokens.remove(i);
                        tokens.add(i, dest);
                    }
                }
            }
            catch (java.lang.Exception e)
            {
            }
            catch (TokenMgrError e)
            {
            }
        }
        return (FormulaToken[]) tokens.toArray(new FormulaToken[tokens.size()]);
    }

    public String printFormula(com.sun.star.sheet.FormulaToken[] aTokens, com.sun.star.table.CellAddress aReferencePos)
    {
        final StringBuffer ret = new StringBuffer();
        for (int i = 0; i < aTokens.length; i++)
        {
            final FormulaToken formulaToken = aTokens[i];
            if (formulaToken.OpCode == opCodePush.Token.OpCode && !formulaToken.Data.equals(Any.VOID))
            {
                ret.append(formulaToken.Data);
            }
            else if (parserAllOpCodes.containsKey(formulaToken.OpCode))
            {
                final FormulaOpCodeMapEntry opCode = (FormulaOpCodeMapEntry) parserAllOpCodes.get(formulaToken.OpCode);
                if (opCode.Name.length() > 0)
                {
                    ret.append(opCode.Name);
                }
                else if (!formulaToken.Data.equals(Any.VOID))
                {
                    ret.append(formulaToken.Data);
                }
            }
        }
        return ret.toString();
    }

    // com.sun.star.beans.XPropertySet:
    public com.sun.star.beans.XPropertySetInfo getPropertySetInfo()
    {
        return m_prophlp.getPropertySetInfo();
    }

    public void setPropertyValue(String aPropertyName, Object aValue) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.beans.PropertyVetoException, com.sun.star.lang.IllegalArgumentException, com.sun.star.lang.WrappedTargetException
    {
        m_prophlp.setPropertyValue(aPropertyName, aValue);
    }

    public Object getPropertyValue(String aPropertyName) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.lang.WrappedTargetException
    {
        return m_prophlp.getPropertyValue(aPropertyName);
    }

    public void addPropertyChangeListener(String aPropertyName, com.sun.star.beans.XPropertyChangeListener xListener) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.lang.WrappedTargetException
    {
        m_prophlp.addPropertyChangeListener(aPropertyName, xListener);
    }

    public void removePropertyChangeListener(String aPropertyName, com.sun.star.beans.XPropertyChangeListener xListener) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.lang.WrappedTargetException
    {
        m_prophlp.removePropertyChangeListener(aPropertyName, xListener);
    }

    public void addVetoableChangeListener(String aPropertyName, com.sun.star.beans.XVetoableChangeListener xListener) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.lang.WrappedTargetException
    {
        m_prophlp.addVetoableChangeListener(aPropertyName, xListener);
    }

    public void removeVetoableChangeListener(String aPropertyName, com.sun.star.beans.XVetoableChangeListener xListener) throws com.sun.star.beans.UnknownPropertyException, com.sun.star.lang.WrappedTargetException
    {
        m_prophlp.removeVetoableChangeListener(aPropertyName, xListener);
    }

    // com.sun.star.report.meta.XFormulaParser:
    public com.sun.star.sheet.FormulaOpCodeMapEntry[] getOpCodeMap()
    {
        return (com.sun.star.sheet.FormulaOpCodeMapEntry[]) m_OpCodeMap.toArray(new FormulaOpCodeMapEntry[m_OpCodeMap.size()]);
    }

    public void setOpCodeMap(com.sun.star.sheet.FormulaOpCodeMapEntry[] the_value)
    {
//        m_prophlp.prepareSet("OpCodeMap", null);
//        synchronized (this)
//        {
//            m_OpCodeMap.clear();
//        }
    }

    public String getImplementationName()
    {
        return SOFormulaParser.class.getName();
    }

    public boolean supportsService(String sServiceName)
    {
        return sServiceName.equals(__serviceName);
    }

    public String[] getSupportedServiceNames()
    {
        return getServiceNames();
    }

    /**
     * This method is a simple helper function to used in the static component initialisation functions as well as
     * in getSupportedServiceNames.
     * @return
     */
    public static String[] getServiceNames()
    {
        return new String[]
                {
                    __serviceName
                };
    }

    public XFormulaOpCodeMapper getFormulaOpCodeMapper()
    {
        if (formulaOpCodeMapper == null)
        {
            formulaOpCodeMapper = new SOFormulaOpCodeMapper(this);
        }

        return formulaOpCodeMapper;
    }

    private void addOpCodes(String[] names, FormulaOpCodeMapEntry[] opCodes, int group)
    {
        addOpCodes(names, opCodes, group, true);
    }

    private void addOpCodes(String[] names, FormulaOpCodeMapEntry[] opCodes, int group, boolean add)
    {
        groupOpCodes[group] = new HashMap();
        for (int j = 0; j < names.length; j++)
        {
            FormulaOpCodeMapEntry opCode = null;
            int i = 0;
            for (; i < opCodes.length; i++)
            {
                opCode = opCodes[i];
                if (names[j].equals(opCode.Name))
                {
                    break;
                }
            }
            if (i >= opCodes.length)
            {
                if (!add)
                {
                    continue;
                }
                final FormulaToken token = new FormulaToken(ownTokenCounter++, Any.VOID);
                opCode = new FormulaOpCodeMapEntry(names[j], token);
            }
            parserNames.put(names[j], opCode);
            parserAllOpCodes.put(opCode.Token.OpCode, opCode);
            groupOpCodes[group].put(opCode.Token.OpCode, opCode);
        }
    }

    public Map getNames()
    {
        return parserNames;
    }

    public Map getGroup(int group)
    {
        return groupOpCodes[group];
    }

    private String[] getOperators(DefaultFormulaContext defaultContext, final String _kind)
    {
        final ArrayList ops = new ArrayList();
        final Configuration configuration = defaultContext.getConfiguration();
        final Iterator iter = configuration.findPropertyKeys(_kind);
        while (iter.hasNext())
        {
            final String configKey = (String) iter.next();
            if (!configKey.endsWith(".class"))
            {
                continue;
            }
            final String operatorClass = configuration.getConfigProperty(configKey);
            if (operatorClass == null)
            {
                continue;
            }
            if (operatorClass.length() == 0)
            {
                continue;
            }
            final String tokenKey = configKey.substring(0, configKey.length() - ".class".length()) + ".token";
            final String token = configuration.getConfigProperty(tokenKey);
            if (token == null)
            {
                continue;
            }
            ops.add(token.trim());
        }
        return (String[]) ops.toArray(new String[ops.size()]);
    }
}

