/*
 * Copyright (c) 2011, 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
//
package org.eclipse.persistence.jpa.jpql.tools.model.query;

import java.io.IOException;
import org.eclipse.persistence.jpa.jpql.Assert;
import org.eclipse.persistence.jpa.jpql.parser.FunctionsReturningStringsBNF;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.LiteralBNF;
import org.eclipse.persistence.jpa.jpql.parser.StringPrimaryBNF;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;

/**
 * The <code><b>TRIM</b></code> function trims the specified character from a string. If the
 * character to be trimmed is not specified, it is assumed to be space (or blank). The optional
 * <code>trim_character</code> is a single-character string literal or a character-valued input
 * parameter (i.e., char or <code>Character</code>). If a trim specification is not provided,
 * <code><b>BOTH</b></code> is assumed. The <code><b>TRIM</b></code> function returns the trimmed
 * string.
 *
 * <div><b>BNF:</b> <code>expression ::= TRIM([[trim_specification] [trim_character] FROM] string_primary)</code><p></div>
 *
 * @see TrimExpression
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings({"nls", "unused"}) // unused used for the import statement: see bug 330740
public class TrimExpressionStateObject extends AbstractSingleEncapsulatedExpressionStateObject {

    /**
     * Defines the way the string is trimmed.
     */
    private Specification specification;

    /**
     * The trim character, if specified.
     */
    private StateObject trimCharacter;

    /**
     * Notifies the visibility of the <code><b>FROM</b></code> identifier has changed.
     */
    public static final String HAS_FROM_PROPERTY = "hasFrom";

    /**
     * Notifies the specification property has changed.
     */
    public static final String SPECIFICATION_PROPERTY = "specification";

    /**
     * Notify the state object representing the trim character has changed.
     */
    public static final String TRIM_CHARACTER_PROPERTY = "trimCharacterStateObject";

    /**
     * Creates a new <code>TrimExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    public TrimExpressionStateObject(StateObject parent) {
        super(parent);
    }

    /**
     * Creates a new <code>TrimExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param stateObject The {@link StateObject} representing the encapsulated expression
     * @param specification Defines the way the string is trimmed, or {@link
     * org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification#DEFAULT
     * Specification.DEFAULT} when it is not present
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    public TrimExpressionStateObject(StateObject parent,
                                     Specification specification,
                                     StateObject stateObject) {

        this(parent, specification, null, stateObject);
    }

    /**
     * Creates a new <code>TrimExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param specification Defines the way the string is trimmed, or {@link
     * org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification#DEFAULT
     * Specification.DEFAULT} when it is not present
     * @param trimCharacter The trim character
     * @param stateObject The {@link StateObject} representing the encapsulated expression
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    public TrimExpressionStateObject(StateObject parent,
                                     Specification specification,
                                     StateObject trimCharacter,
                                     StateObject stateObject) {

        super(parent, stateObject);
        this.specification = specification;
        this.trimCharacter = parent(trimCharacter);
    }

    /**
     * Creates a new <code>TrimExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param jpqlFragment The portion of the query representing the encapsulated expression
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    public TrimExpressionStateObject(StateObject parent, String jpqlFragment) {
        super(parent, jpqlFragment);
    }

    /**
     * {@inheritDoc}
     */
    public void accept(StateObjectVisitor visitor) {
        visitor.visit(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TrimExpression getExpression() {
        return (TrimExpression) super.getExpression();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getIdentifier() {
        return TRIM;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getQueryBNFId() {
        return StringPrimaryBNF.ID;
    }

    /**
     * Returns the new trim specification.
     *
     * @return The new trim specification; which is never <code>null</code>
     */
    public Specification getSpecification() {
        return specification;
    }

    /**
     * Returns the {@link StateObject} representing the trim character.
     *
     * @return The {@link StateObject} representing the trim character or <code>null</code> if it is
     * not present
     */
    public StateObject getTrimCharacter() {
        return trimCharacter;
    }

    /**
     * Determines whether the way the trim is trimmed was parsed.
     *
     * @return <code>true</code> if the query contained the way the trim needs to be trimmed;
     * <code>false</code> otherwise
     */
    public boolean hasSpecification() {
        return (specification != Specification.DEFAULT);
    }

    /**
     * Determines whether the character used to trim the string was specified.
     *
     * @return <code>true</code> if the character used for trimming was specified; <code>false</code>
     * otherwise
     */
    public boolean hasTrimCharacter() {
        return trimCharacter != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEquivalent(StateObject stateObject) {

        if (super.isEquivalent(stateObject)) {
            TrimExpressionStateObject trim = (TrimExpressionStateObject) stateObject;
            return specification == trim.specification &&
                   areEquivalent(trimCharacter, trim.trimCharacter);
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void parse(String jpqlFragment) {

        StringBuilder sb = new StringBuilder();
        sb.append(TRIM);
        sb.append(LEFT_PARENTHESIS);
        sb.append(jpqlFragment);
        sb.append(RIGHT_PARENTHESIS);

        JPQLExpression jpqlExpression = new JPQLExpression(
            sb,
            getGrammar(),
            FunctionsReturningStringsBNF.ID,
            true
        );

        TrimExpression trimExpression = (TrimExpression) jpqlExpression.getQueryStatement();
        setSpecification(trimExpression.getSpecification());
        parseTrimCharacter(trimExpression.getTrimCharacter().toParsedText());
        super.parse(trimExpression.getExpression().toParsedText());

        // The trim character is actually the string primary
        if (!hasStateObject() && hasTrimCharacter()) {
            setStateObject(new StringLiteralStateObject(this, trimCharacter.toString()));
            trimCharacter = null;
        }
    }

    /**
     * Parses the given JPQL fragment, which represents either a single-character string literal or a
     * character-valued input parameter, the fragment will be parsed and converted into a {@link
     * StateObject}.
     *
     * @param jpqlFragment The portion of the query to parse
     */
    public void parseTrimCharacter(CharSequence jpqlFragment) {
        StateObject stateObject = buildStateObject(jpqlFragment, LiteralBNF.ID);
        setTrimCharacter(stateObject);
    }

    /**
     * Removes the trim specification.
     */
    public void removeSpecification() {
        setSpecification(Specification.DEFAULT);
    }

    /**
     * Removes the trim character if it is defined.
     */
    public void removeTrimCharacter() {
        setTrimCharacter(null);
    }

    /**
     * Keeps a reference of the {@link TrimExpression parsed object} object, which should only be
     * done when this object is instantiated during the conversion of a parsed JPQL query into
     * {@link StateObject StateObjects}.
     *
     * @param expression The {@link TrimExpression parsed object} representing a <code><b>TRIM</b></code>
     * expression
     */
    public void setExpression(TrimExpression expression) {
        super.setExpression(expression);
    }

    /**
     * Sets the new trim specification.
     *
     * @param specification The new trim specification; <code>null</code> is not valid
     */
    public void setSpecification(Specification specification) {
        Assert.isNotNull(specification, "The Specification cannot be null");
        Specification oldSpecification = this.specification;
        this.specification = specification;
        firePropertyChanged(SPECIFICATION_PROPERTY, oldSpecification, specification);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setStateObject(StateObject stateObject) {
        super.setStateObject(stateObject);
    }

    /**
     * Sets the character to trim from the string. If the character to be trimmed is not specified,
     * it is assumed to be space (or blank). It is a single-character string literal or a character-
     * valued input parameter (i.e., char or <code>Character</code>).
     *
     * @param trimCharacter The trim character or <code>null</code> to remove it
     */
    public void setTrimCharacter(StateObject trimCharacter) {
        StateObject oldTrimCharacter = this.trimCharacter;
        this.trimCharacter = parent(trimCharacter);
        firePropertyChanged(TRIM_CHARACTER_PROPERTY, oldTrimCharacter, trimCharacter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void toTextEncapsulatedExpression(Appendable writer) throws IOException {

        // Specification
        if (specification != Specification.DEFAULT) {
            writer.append(specification.name());
            writer.append(SPACE);
        }

        // Trim character
        if (hasTrimCharacter()) {
            trimCharacter.toString(writer);
            writer.append(SPACE);
        }

        // FROM
        if ((specification != Specification.DEFAULT) || hasTrimCharacter()) {
            writer.append(FROM);
            writer.append(SPACE);
        }

        // String primary
        super.toTextEncapsulatedExpression(writer);
    }
}
