/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.sdo.helper;

import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.sdo.SDOConstants;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.SDOException;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Sequence;
import commonj.sdo.helper.DataHelper;
import commonj.sdo.impl.HelperProvider;

/**
 * <p><b>Purpose</b>: Perform operations based on SDO XPath against DataObjects.
 * </p>
 */

public class XPathEngine {
    private static final int SET = 1;
    private static final int ISSET = 2;
    private static final int UNSET = 3;
    protected static XPathEngine defaultXPathEngine;

    private XPathEngine() {
    }

    public static XPathEngine getInstance() {
        if (defaultXPathEngine == null) {
            defaultXPathEngine = new XPathEngine();
        }
        return defaultXPathEngine;
    }

    /**Handle queries about setting status of a property through path base access.
     *
     * @param path      the String representation of path based access
     * @param caller    the DataObject that pass path information in
     * @return          true if queried property is set, otherwise false
     */
    public boolean isSet(String path, DataObject caller) {
        try {
            return processPath(path, null, caller, false, ISSET);
            // return lastDataObject.isSet(lastProperty);// spec. did not mention exception
        } catch (Exception e) {
            return false;// spec. did not mention exception
        }
    }

    /**Unset the value of a property through the path base access.
     *
     * @param path      the String representation of path based access
     * @param caller    the DataObject that pass path information in
     */
    public void unset(String path, DataObject caller) {
        processPath(path, null, caller, false, UNSET);
    }

    /**Set a property's value through the path base access.
     *
     * @param path      the String representation of path based access
     * @param value     the value to be set as the target property's value
     * @param caller    the DataObject that pass path information in
     * @param convertValue     boolean used for set if we should convert the value
     */
    public void set(String path, Object value, DataObject caller, boolean convertValue) {
        processPath(path, value, caller, convertValue, SET);
    }

    /**When accessing values corresponding to properties of DataObject by path base accessors,
     * the accessed getters will pass informations to this method to process information and
     * acquire wanted values.
     *
     * @param path          the String representation of path based access
     * @param caller        the DataObject that pass path information in
     * @return              the value gotten by accessing through path
     */
    public Object get(String path, DataObject caller) {// path like "a/b/c"
        if ((path == null) || path.equals(SDOConstants.EMPTY_STRING)) {
            return null;
        }
        if (path.equals("..")) {
            return caller.getContainer();
        }
        if (path.equals(SDOConstants.SDO_XPATH_TO_ROOT)) {
            return ((SDODataObject)caller).getRootObject();
        }
        path = getLocalName(path);

        int index = path.indexOf('/');
        if (index > -1) {
            int openBracketIndex = path.indexOf('[');
            int closeBracketIndex = path.indexOf(']');
            if(index > openBracketIndex && index < closeBracketIndex) {
                return getValueForFragment(path, caller);
            }
            if (index == (path.length() - 1)) {
                return getValueForFragment(path.substring(0, index), caller);
            } else {
                Object value = getValueForFragment(path.substring(0, index), caller);
                DataObject currentDataObject = (DataObject) value;
                return get(path.substring(index + 1, path.length()), currentDataObject);
            }
        }
        return getValueForFragment(path, caller);
    }

        /**extract wanted fragment from the string representation of path and pass processed result to
     * method setIsSetUnSet for further operation.
     *
     * @param path      the String representation of path based access
     * @param value     the value to be set as the target property's value
     * @param caller    the DataObject that pass path information in
     * @param convertValue     boolean used for set if we should convert the value
     * @param _case     an int value indicating what kind of operation to use: set, isset or unset.
     * @return          true if operation is isset and property's value is set, otherwise false.
     */
    private boolean processPath(String path, Object value, DataObject caller, boolean convertValue, int _case) {
        path = getLocalName(path);
        int lastSlashIndex = path.lastIndexOf('/');
        String frag;
        if (-1 < lastSlashIndex) {// case 1: a/b/c
            frag = path.substring(lastSlashIndex + 1);
            return setIsSetUnSet(frag, path, caller, value, lastSlashIndex, convertValue, _case);
        } else {// case 2 : "a"
            frag = path;
            return setIsSetUnSet(frag, path, caller, value, lastSlashIndex, convertValue, _case);
        }
    }

    /**According to the requirement, correspondingly perform isset, unset or set function.
     *
     * @param frag              one string fragment in the path
     * @param path              the String representation of path based access
     * @param caller            the DataObject that pass path information in
     * @param value             the value to be set as the target property's value
     * @param lastSlashIndex    the last index of '/' in the path string
     * @param convertValue     boolean used for set if we should convert the value
     * @param _case             an int value indicating what kind of operation to use: set, isset or unset.
     * @return                  true if operation is isset and property's value is set, otherwise false.
     */
    private boolean setIsSetUnSet(String frag, String path, DataObject caller, Object value, int lastSlashIndex, boolean convertValue, int _case) {
        int indexOfDot = frag.lastIndexOf('.');
        int indexOfOpenBracket = frag.lastIndexOf('[');
        int indexOfCloseBracket = frag.lastIndexOf(']');
        int numInLastProperty = getNumberInFrag(frag, indexOfDot, indexOfOpenBracket, indexOfCloseBracket);
        String lastPropertyName = getPropertyNameInFrag(frag, numInLastProperty, indexOfDot, indexOfOpenBracket);// get last property name on path for case 1
        DataObject lastDataObject;
        if (-1 < lastSlashIndex) {
            Object lastObject = get(path.substring(0, lastSlashIndex), caller);// get last dataobject on path
            // If trying to access a list element from a null list, this object will be
            // an instance of ListWrapper, not DataObject, but the error message is the same
            // as if it was a null DataObject
            if (lastObject == null || lastObject instanceof ListWrapper) {
                throw SDOException.cannotPerformOperationOnProperty(lastPropertyName, path);
            }
            lastDataObject = (SDODataObject) lastObject;
        } else {
            lastDataObject = caller;
        }
        Property lastProperty = lastDataObject.getInstanceProperty(lastPropertyName);// get property of this dataobject

        switch (_case) {
        case SET:
            if (lastProperty == null) {
                lastProperty = ((SDODataObject)lastDataObject).defineOpenContentProperty(lastPropertyName, value);
            }if(lastProperty != null){
              set(lastProperty, lastDataObject, numInLastProperty, value,convertValue);
            }
            return false;
        case ISSET:
            if(lastProperty == null){
             return false;
            }
            return isSet(lastProperty, lastDataObject);
        case UNSET:
            if(lastProperty == null){
             return false;
            }
            unSet(lastProperty, lastDataObject, numInLastProperty);
            return false;
        default:
            return false;
        }
    }

    /** Method that returns whether a property is set.
     *
     * @param lastProperty      the property to queries.
     * @param lastDataObject    the DataObject, owner of the queried property
     * @return      return true, if property's value is set, otherwise false.
     */
    private boolean isSet(Property lastProperty, DataObject lastDataObject) {
        return lastDataObject.isSet(lastProperty);
    }

    /** Method that unset a certain property's value.
     *
     * @param lastProperty      the property to queries.
     * @param lastDataObject    the DataObject, owner of the queried property
     */
    private void unSet(Property lastProperty, DataObject lastDataObject, int numInLastProperty) {
        if (numInLastProperty == -1) {
            lastDataObject.unset(lastProperty);
        } else {
            List objects = lastDataObject.getList(lastProperty);
            if (numInLastProperty <= objects.size()) {
                objects.remove(numInLastProperty);
            }
        }
    }

    /** Set a property's value.
     *
     * @param lastProperty      the property to queries.
     * @param lastDataObject    the DataObject, owner of the queried property
     * @param numInLastProperty the index number in the value list of the above property
     * @param value             the value to be set as the target property's value
     * @param convertValue     boolean used for set if we should convert the value
     */
    private void set(Property lastProperty, DataObject lastDataObject, int numInLastProperty, Object value, boolean convertValue) {
        if (numInLastProperty == -1) {
            if (lastDataObject != null) {
                if(convertValue){
                  DataHelper dataHelper = ((SDODataObject)lastDataObject).getType().getHelperContext().getDataHelper();
                  value = dataHelper.convert(lastProperty, value);
                }
                lastDataObject.set(lastProperty, value);
            } else {
                throw new IllegalArgumentException("lastDataObject is null");
            }
        } else {// case like set("a/b.1", List) will cause a list be added into a existed list
            List objects = lastDataObject.getList(lastProperty);

            if (convertValue) {
                DataHelper dataHelper = ((SDODataObject)lastDataObject).getType().getHelperContext().getDataHelper();
                value =  dataHelper.convert(lastProperty.getType(), value);
            }

            Sequence seq = lastDataObject.getSequence();
            if (seq != null) {
                seq.setValue(numInLastProperty, value);
            } else {
                objects.set(numInLastProperty, value);
            }
        }
    }

    private String getLocalName(String qualifiedName) {
        int index = qualifiedName.indexOf(':');
        if (index > -1) {
            int bracketIndex = qualifiedName.indexOf('[');
            if(bracketIndex > -1 && bracketIndex < index) {
                return qualifiedName;
            }
            String local = qualifiedName.substring(index + 1, qualifiedName.length());
            return local;
        } else {
            return qualifiedName;
        }
    }

    /** Process the passed in fragment, extract the position information if available,
     *  acquire the property name hidden in this fragment and check if this fragment is
     *  actually query base. Then perform corresponding actions to access values.
     *
     * @param frag      one string fragment in the path
     * @param caller    a DataObject that originally took the path information
     * @return          values to be accessed
     */
    private Object getValueForFragment(String frag, DataObject caller) {
        int indexOfDot = frag.lastIndexOf('.');
        int indexOfOpenBracket = frag.indexOf('[');
        int indexOfCloseBracket = frag.lastIndexOf(']');
        int position = getNumberInFrag(frag, indexOfDot, indexOfOpenBracket, indexOfCloseBracket);
        String propertyName = getPropertyNameInFrag(frag, position, indexOfDot, indexOfOpenBracket);
        int equalSignIndex = isQueryPath(frag, indexOfOpenBracket, indexOfCloseBracket);
        if (equalSignIndex == -1) {// not query path, note:
            return getObjectByFragment(propertyName, position, caller);
        }
        return getDataObjectFromQuery(frag, indexOfOpenBracket, indexOfCloseBracket, equalSignIndex, caller, propertyName);
    }

    /** Extract the property name hidden in a fragment of path
     *
     * @param frag          one string fragment in the path
     * @param position      the index of values to be accessed
     * @param indexOfDot    the index of . in the fragment
     * @param indexOfOpenBracket    the indexof [ in the fragment
     * @return              the property name hidden in this fragment
     */
    private String getPropertyNameInFrag(String frag, int position, int indexOfDot, int indexOfOpenBracket) {
        int startIndex = 0;
        int atIndex = frag.indexOf('@');
        if (atIndex != -1 && (indexOfOpenBracket == -1 || atIndex < indexOfOpenBracket)) {
            startIndex += 1;
        }
        if (indexOfOpenBracket != -1) {
            return frag.substring(startIndex, indexOfOpenBracket);
        }
        if ((indexOfDot != -1) && (position != -1)) {
            return frag.substring(startIndex, indexOfDot);
        }
        return frag.substring(startIndex);
    }

    /** Judge if positional path belongs to bracket case or dot case, then perform
     * different actions.
     *
     * @param frag                  one string fragment in the path
     * @param indexOfDot            the index of . in the fragment
     * @param indexOfOpenBracket    the index of [ in the fragment
     * @param indexOfCloseBracket   the index of ] in the fragment
     * @return      the position hidden in fragment
     */
    private int getNumberInFrag(String frag, int indexOfDot, int indexOfOpenBracket, int indexOfCloseBracket) {
        if ((indexOfOpenBracket != -1) && (indexOfCloseBracket != -1) && (indexOfOpenBracket < indexOfCloseBracket)) {
            return acquireNumberInBrackets(frag, indexOfOpenBracket, indexOfCloseBracket);
        }
        if (indexOfDot != -1) {
            return acquireNumberAtDot(frag, indexOfDot);
        }
        return -1;
    }

    /**check if information in brackets is qury or not.
     *
     * @param frag                 a fragment in path
     * @param openBracketIndex     index of open bracket in fragment
     * @param closeBracketIndex    index of close bracket in fragment
     * @return
     */
    private int isQueryPath(String frag, int openBracketIndex, int closeBracketIndex) {
        if ((openBracketIndex != -1) && (closeBracketIndex != -1) && (openBracketIndex < closeBracketIndex)) {
            return frag.substring(openBracketIndex, closeBracketIndex).indexOf('=');
        }
        return -1;
    }

    // extract value from query and acquire dataobject that meets this requirement

    /**Access the DataObject value by using the fragment containing query informations.
     *
     * @param frag                  one string fragment in the path
     * @param openBracketIndex      the index of open bracket in a fragment
     * @param closeBracketIndex     the index of close bracket in a fragment
     * @param equalsignIndex        the index of equalsign in string fragment quoted by brackets
     * @param caller                the DataObject that passes the path information in
     * @param callerProperty        the name of the property
     * @return                      the DataObject as value of the property having name as the above callerProperty
     */
    private DataObject getDataObjectFromQuery(String frag, int openBracketIndex, int closeBracketIndex, int equalsignIndex, DataObject caller, String callerProperty) {
        try {
            // trim off any whitespace for property names
            String propertyNameOfQryDataObject = frag.substring(openBracketIndex + 1, equalsignIndex + openBracketIndex).trim();
            List objects = caller.getList(caller.getInstanceProperty(callerProperty));
            String query = frag.substring(equalsignIndex + openBracketIndex + 1, closeBracketIndex);
            String value = null;
            int firstQuoteIndex = query.indexOf('\'');
            int lastQuoteIndex = query.lastIndexOf('\'');
            if ((firstQuoteIndex == -1) && (lastQuoteIndex == -1)) {// !! note: case: [number=1'23'] is assume not to happen !!
                firstQuoteIndex = query.indexOf("\"");
                lastQuoteIndex = query.lastIndexOf("\"");
            }
            if ((firstQuoteIndex != -1) && (lastQuoteIndex != -1) && (firstQuoteIndex < lastQuoteIndex)) {// quoted string existed
                value = query.substring(firstQuoteIndex + 1, lastQuoteIndex);
            } else {
                // if the value is not enclosed on quotes, trim off any whitespace
                value = query.trim();
            }
            Iterator iterObjects = objects.iterator();
            Object queryValue = value;
            Object actualValue = null;
            while (iterObjects.hasNext()) {
                DataObject cur = (DataObject)iterObjects.next();
                Property p = cur.getInstanceProperty(propertyNameOfQryDataObject);
                if(p != null){
                  try {
                      queryValue = XMLConversionManager.getDefaultXMLManager().convertObject(queryValue, p.getType().getInstanceClass());
                  } catch (ConversionException e) {
                      //do nothing, skip
                  }

                  if (!p.isMany()) {
                      actualValue = cur.get(p);
                      if (actualValue.equals(queryValue)) {
                          return cur;
                      }
                  } else {// case p is many type
                      List values = cur.getList(p);
                      Iterator iterValues = values.iterator();

                      while (iterValues.hasNext()) {
                          actualValue = iterValues.next();
                          if (actualValue.equals(queryValue)) {
                              return cur;
                          }
                      }
                  }
                }
            }

            return null;
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    // process a portion of path, find out if it is positional or not.
    // if it is, then check it is using '.' or '[ ]' and perform corresponding
    // process.

    /** Check if position exists, then either acquire value through position or directly.
     *
     * @param propertyName      the name of property of the caller
     * @param position          the index of values to be accessed
     * @param caller            the DataObject containing property with the passed in property name
     * @return                  the values to be accessed
     */
    private Object getObjectByFragment(String propertyName, int position, DataObject caller) {
        Property prop = caller.getInstanceProperty(propertyName);
        if (prop == null){
          return null;
        }

        if (prop.isMany() && position > -1) {
            return caller.getList(prop).get(position);
        } else {
            return caller.get(prop);
        }
    }

    /** Extract position (index) from a bracket fragment of path
     *
     * @param frag               a fragment of path
     * @param openBracketIndex   the index of [ in path
     * @param closeBracketIndex  the index of ] in path
     * @return                   the index hidden in fragment
     */
    private int acquireNumberInBrackets(String frag, int openBracketIndex, int closeBracketIndex) {
        String number = frag.substring(openBracketIndex + 1, closeBracketIndex);

        if (number.matches("[1-9][0-9]*")) {
            return Integer.parseInt(number) - 1;
            //value = number;
        }
        return -1;// throw Illegal path?
    }

    /**Extract the position (index) from a dot fragment of path
     *
     * @param frag      a fragment of path
     * @param dotIndex  the index of . in path
     * @return          the index hidden in the fragment
     */
    private int acquireNumberAtDot(String frag, int dotIndex) {
        int value = -1;
        String position = frag.substring(dotIndex + 1);
        if (position.matches("[0-9]+")) {
            value = Integer.parseInt(position);
            //value = position;
        }
        return value;
    }

    // dataobject a's property a has value dataobject b, dataobject b's property b has value dataobject c,
    //dataobject c's property c has value boolean,

    /** access the wanted values through path and convert it into required java class.
     * If conversion is not supported, exception is thrown.
     *
     * @param path      string representation of accessing path
     * @param cls       the java class that accessed value is to be converted to
     * @param caller    the DataObject that pass the path in
     * @return          values to be accessed
     * @throws ClassCastException
     */
    public Object convertObjectToValueByPath(String path, Class cls, DataObject caller) throws ClassCastException {
        if (null == path || XMLConstants.EMPTY_STRING.equals(path)) {
            throw new ClassCastException("Attempting null value conversion.");
        }
        try {
            int lastSlashIndex = path.lastIndexOf('/');
            SDODataObject lastDataObject;
            String lastPropertyName;
            Property lastProperty;
            int numInLastProperty = -1;

            // to do: if "/" or ".." lastDataObject = container or root
            if (-1 < lastSlashIndex) {// case 1 "a/b/c"
                String frag = path.substring(lastSlashIndex + 1);
                int indexOfDot = frag.lastIndexOf('.');
                int indexOfOpenBracket = frag.lastIndexOf('[');
                int indexOfCloseBracket = frag.lastIndexOf(']');
                numInLastProperty = getNumberInFrag(frag, indexOfDot, indexOfOpenBracket, indexOfCloseBracket);
                lastPropertyName = getPropertyNameInFrag(frag, numInLastProperty, indexOfDot, indexOfOpenBracket);//getPropertyNameFromFragment(path.substring(lastSlashIndex + 1));// get last property name on path for case 1
                lastDataObject = (SDODataObject)caller.getDataObject(path.substring(0, lastSlashIndex));// get last dataobject on path
                if(lastDataObject == null){
                  return null;
                }
                lastProperty = lastDataObject.getInstanceProperty(lastPropertyName);// get property of this dataobject
            } else {// case 2 "a"
                //to do: call eextractPositionAndPropertyName() here
                String frag = path;
                int indexOfDot = frag.lastIndexOf('.');
                int indexOfOpenBracket = frag.lastIndexOf('[');
                int indexOfCloseBracket = frag.lastIndexOf(']');
                numInLastProperty = getNumberInFrag(frag, indexOfDot, indexOfOpenBracket, indexOfCloseBracket);
                lastPropertyName = getPropertyNameInFrag(frag, numInLastProperty, indexOfDot, indexOfOpenBracket);//getPropertyNameFromFragment(path);// get last property name on path for case 1
                lastDataObject = (SDODataObject)caller;
                if(lastDataObject == null){
                  return null;
                }
                lastProperty = caller.getInstanceProperty(lastPropertyName);// get property of this dataobject
            }

            if ((lastProperty != null) && (cls == Date.class) && lastProperty.getType().equals(SDOConstants.SDO_STRING)) {// in case getDate, for string property, use DataHelper
                DataHelper dHelper = HelperProvider.getDefaultContext().getDataHelper();
                String dateString;
                if (numInLastProperty == -1) {
                    dateString = (String)lastDataObject.get(lastProperty);
                } else {
                    dateString = (String)(lastDataObject.getList(lastProperty)).get(numInLastProperty);
                }
                return dHelper.toDate(dateString);
            }
            return lastDataObject.convertObjectToValue(lastProperty, numInLastProperty, cls);
        } catch (IllegalArgumentException e) {// according to exception table 1st row, when get(Property) and get(Index) throw IllegalArgument, get(String) return null
            throw new ClassCastException("Conversion is not supported.");
            //return null;
        }
    }
}
