/*-
 * #%L
 * JSQLParser library
 * %%
 * Copyright (C) 2004 - 2019 JSQLParser
 * %%
 * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
 * #L%
 */
package net.sf.jsqlparser.expression;

import java.util.List;
import java.util.Objects;

import net.sf.jsqlparser.statement.select.OrderByElement;

/**
 * @author <a href="mailto:andreas@manticore-projects.com">Andreas Reichel</a>
 */

public class JsonAggregateFunction extends FilterOverImpl implements Expression {
    private final OrderByClause expressionOrderBy = new OrderByClause();
    private JsonFunctionType functionType;
    private Expression expression = null;
    private boolean usingKeyKeyword = false;
    private Object key;
    private boolean usingValueKeyword = false;
    private Object value;

    private boolean usingFormatJson = false;

    private JsonAggregateOnNullType onNullType;
    private JsonAggregateUniqueKeysType uniqueKeysType;


    public JsonAggregateOnNullType getOnNullType() {
        return onNullType;
    }

    public void setOnNullType(JsonAggregateOnNullType onNullType) {
        this.onNullType = onNullType;
    }

    public JsonAggregateFunction withOnNullType(JsonAggregateOnNullType onNullType) {
        this.setOnNullType(onNullType);
        return this;
    }

    public JsonAggregateUniqueKeysType getUniqueKeysType() {
        return uniqueKeysType;
    }

    public void setUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) {
        this.uniqueKeysType = uniqueKeysType;
    }

    public JsonAggregateFunction withUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) {
        this.setUniqueKeysType(uniqueKeysType);
        return this;
    }

    public JsonFunctionType getType() {
        return functionType;
    }

    public void setType(JsonFunctionType type) {
        this.functionType = Objects.requireNonNull(type,
                "The Type of the JSON Aggregate Function must not be null");
    }

    public void setType(String typeName) {
        this.functionType = JsonFunctionType
                .valueOf(Objects
                        .requireNonNull(typeName,
                                "The Type of the JSON Aggregate Function must not be null")
                        .toUpperCase());
    }

    public JsonAggregateFunction withType(JsonFunctionType type) {
        this.setType(type);
        return this;
    }

    public JsonAggregateFunction withType(String typeName) {
        this.setType(typeName);
        return this;
    }

    public Expression getExpression() {
        return expression;
    }

    public void setExpression(Expression expression) {
        this.expression = expression;
    }

    public JsonAggregateFunction withExpression(Expression expression) {
        this.setExpression(expression);
        return this;
    }

    public boolean isUsingKeyKeyword() {
        return usingKeyKeyword;
    }

    public void setUsingKeyKeyword(boolean usingKeyKeyword) {
        this.usingKeyKeyword = usingKeyKeyword;
    }

    public JsonAggregateFunction withUsingKeyKeyword(boolean usingKeyKeyword) {
        this.setUsingKeyKeyword(usingKeyKeyword);
        return this;
    }

    public Object getKey() {
        return key;
    }

    public void setKey(Object key) {
        this.key = key;
    }

    public JsonAggregateFunction withKey(Object key) {
        this.setKey(key);
        return this;
    }

    public boolean isUsingValueKeyword() {
        return usingValueKeyword;
    }

    public void setUsingValueKeyword(boolean usingValueKeyword) {
        this.usingValueKeyword = usingValueKeyword;
    }

    public JsonAggregateFunction withUsingValueKeyword(boolean usingValueKeyword) {
        this.setUsingValueKeyword(usingValueKeyword);
        return this;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public JsonAggregateFunction withValue(Object value) {
        this.setValue(value);
        return this;
    }

    public boolean isUsingFormatJson() {
        return usingFormatJson;
    }

    public void setUsingFormatJson(boolean usingFormatJson) {
        this.usingFormatJson = usingFormatJson;
    }

    public JsonAggregateFunction withUsingFormatJson(boolean usingFormatJson) {
        this.setUsingFormatJson(usingFormatJson);
        return this;
    }

    public List<OrderByElement> getExpressionOrderByElements() {
        return expressionOrderBy.getOrderByElements();
    }

    public void setExpressionOrderByElements(List<OrderByElement> orderByElements) {
        expressionOrderBy.setOrderByElements(orderByElements);
    }

    public JsonAggregateFunction withExpressionOrderByElements(
            List<OrderByElement> orderByElements) {
        this.setExpressionOrderByElements(orderByElements);
        return this;
    }

    @Override
    public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
        return expressionVisitor.visit(this, context);
    }

    // avoid countless Builder --> String conversion
    @Override
    public StringBuilder append(StringBuilder builder) {
        switch (functionType) {
            case OBJECT:
            case MYSQL_OBJECT:
                appendObject(builder);
                break;
            case ARRAY:
                appendArray(builder);
                break;
            default:
                // this should never happen really
                throw new UnsupportedOperationException("JSON Aggregate Function of the type "
                        + functionType.name() + " has not been implemented yet.");
        }
        return builder;
    }

    @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
    public StringBuilder appendObject(StringBuilder builder) {
        builder.append("JSON_OBJECTAGG( ");
        if (usingValueKeyword) {
            if (usingKeyKeyword) {
                builder.append("KEY ");
            }
            builder.append(key).append(" VALUE ").append(value);
        } else if (functionType == JsonFunctionType.MYSQL_OBJECT) {
            builder.append(key).append(", ").append(value);
        } else {
            builder.append(key).append(":").append(value);
        }

        if (usingFormatJson) {
            builder.append(" FORMAT JSON");
        }

        if (onNullType != null) {
            switch (onNullType) {
                case NULL:
                    builder.append(" NULL ON NULL");
                    break;
                case ABSENT:
                    builder.append(" ABSENT On NULL");
                    break;
                default:
                    // this should never happen
            }
        }

        if (uniqueKeysType != null) {
            switch (uniqueKeysType) {
                case WITH:
                    builder.append(" WITH UNIQUE KEYS");
                    break;
                case WITHOUT:
                    builder.append(" WITHOUT UNIQUE KEYS");
                    break;
                default:
                    // this should never happen
            }
        }

        builder.append(" ) ");


        // FILTER( WHERE expression ) OVER windowNameOrSpecification
        super.append(builder);

        return builder;
    }

    @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
    public StringBuilder appendArray(StringBuilder builder) {
        builder.append("JSON_ARRAYAGG( ");
        builder.append(expression).append(" ");

        if (usingFormatJson) {
            builder.append("FORMAT JSON ");
        }

        expressionOrderBy.toStringOrderByElements(builder);

        if (onNullType != null) {
            switch (onNullType) {
                case NULL:
                    builder.append(" NULL ON NULL ");
                    break;
                case ABSENT:
                    builder.append(" ABSENT On NULL ");
                    break;
                default:
                    // "ON NULL" was ommitted
            }
        }
        builder.append(") ");


        // FILTER( WHERE expression ) OVER windowNameOrSpecification
        super.append(builder);

        return builder;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        return append(builder).toString();
    }
}
