/*-
 * #%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 net.sf.jsqlparser.parser.ASTNodeAccessImpl;
import net.sf.jsqlparser.statement.create.table.ColDataType;
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.statement.select.Select;

import java.util.ArrayList;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CastExpression extends ASTNodeAccessImpl implements Expression {
    private final static Pattern PATTERN =
            Pattern.compile("(^[a-z0-9_]*){1}", Pattern.CASE_INSENSITIVE);
    public String keyword;

    private Expression leftExpression;
    private ColDataType colDataType = null;
    private ArrayList<ColumnDefinition> columnDefinitions = new ArrayList<>();

    private boolean isImplicitCast = false;

    // BigQuery specific FORMAT clause:
    // https://cloud.google.com/bigquery/docs/reference/standard-sql/conversion_functions#cast_as_date
    private String format = null;

    public CastExpression(String keyword, Expression leftExpression, String dataType) {
        this.keyword = keyword;
        this.leftExpression = leftExpression;
        this.colDataType = new ColDataType(dataType);
    }

    // Implicit Cast
    public CastExpression(String dataType, String value) {
        this.keyword = null;
        this.isImplicitCast = true;
        this.colDataType = new ColDataType(dataType);
        this.leftExpression = new StringValue(value);
    }

    public CastExpression(ColDataType colDataType, String value) {
        this.keyword = null;
        this.isImplicitCast = true;
        this.colDataType = colDataType;
        this.leftExpression = new StringValue(value);
    }

    public CastExpression(ColDataType colDataType, Long value) {
        this.keyword = null;
        this.isImplicitCast = true;
        this.colDataType = colDataType;
        this.leftExpression = new LongValue(value);
    }

    public CastExpression(ColDataType colDataType, Double value) {
        this.keyword = null;
        this.isImplicitCast = true;
        this.colDataType = colDataType;
        this.leftExpression = new DoubleValue(value);
    }

    public CastExpression(Expression leftExpression, String dataType) {
        this.keyword = null;
        this.leftExpression = leftExpression;
        this.colDataType = new ColDataType(dataType);
    }


    public CastExpression(String keyword) {
        this.keyword = keyword;
    }

    public CastExpression() {
        this("CAST");
    }

    public static boolean isOf(ColDataType colDataType, DataType... types) {
        return Set.of(types).contains(DataType.from(colDataType.getDataType()));
    }

    public static boolean isTime(ColDataType colDataType) {
        return isOf(colDataType, DataType.TIME, DataType.TIME_WITH_TIME_ZONE,
                DataType.TIME_WITHOUT_TIME_ZONE);
    }

    public static boolean isTimeStamp(ColDataType colDataType) {
        return isOf(colDataType, DataType.TIMESTAMP_NS, DataType.TIMESTAMP,
                DataType.TIMESTAMP_WITHOUT_TIME_ZONE,
                DataType.DATETIME, DataType.TIMESTAMP_MS, DataType.TIMESTAMP_S,
                DataType.TIMESTAMPTZ, DataType.TIMESTAMP_WITH_TIME_ZONE);
    }

    public static boolean isDate(ColDataType colDataType) {
        return isOf(colDataType, DataType.DATE);
    }

    public static boolean isBLOB(ColDataType colDataType) {
        return isOf(colDataType, DataType.BLOB, DataType.BYTEA, DataType.BINARY, DataType.VARBINARY,
                DataType.BYTES, DataType.VARBYTE);
    }

    public static boolean isFloat(ColDataType colDataType) {
        return isOf(colDataType, DataType.REAL, DataType.FLOAT4, DataType.FLOAT, DataType.DOUBLE,
                DataType.DOUBLE_PRECISION, DataType.FLOAT8);
    }

    public static boolean isInteger(ColDataType colDataType) {
        return isOf(colDataType, DataType.TINYINT, DataType.INT1, DataType.SMALLINT, DataType.INT2,
                DataType.SHORT, DataType.INTEGER, DataType.INT4, DataType.INT, DataType.SIGNED,
                DataType.BIGINT, DataType.INT8, DataType.LONG, DataType.HUGEINT, DataType.UTINYINT,
                DataType.USMALLINT, DataType.UINTEGER, DataType.UBIGINT, DataType.UHUGEINT);
    }

    public static boolean isDecimal(ColDataType colDataType) {
        return isOf(colDataType, DataType.DECIMAL, DataType.NUMBER, DataType.NUMERIC);
    }

    public static boolean isText(ColDataType colDataType) {
        return isOf(colDataType, DataType.VARCHAR, DataType.NVARCHAR, DataType.CHAR, DataType.NCHAR,
                DataType.BPCHAR, DataType.STRING, DataType.TEXT, DataType.CLOB);
    }

    public ColDataType getColDataType() {
        return colDataType;
    }

    public void setColDataType(ColDataType colDataType) {
        this.colDataType = colDataType;
    }

    public ArrayList<ColumnDefinition> getColumnDefinitions() {
        return columnDefinitions;
    }

    public void addColumnDefinition(ColumnDefinition columnDefinition) {
        this.columnDefinitions.add(columnDefinition);
    }

    public Expression getLeftExpression() {
        return leftExpression;
    }

    public void setLeftExpression(Expression expression) {
        leftExpression = expression;
    }

    public boolean isImplicitCast() {
        return isImplicitCast;
    }

    public CastExpression setImplicitCast(boolean implicitCast) {
        isImplicitCast = implicitCast;
        return this;
    }

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

    @Deprecated
    public boolean isUseCastKeyword() {
        return keyword != null && !keyword.isEmpty();
    }

    @Deprecated
    public void setUseCastKeyword(boolean useCastKeyword) {
        if (useCastKeyword) {
            if (keyword == null || keyword.isEmpty()) {
                keyword = "CAST";
            }
        } else {
            keyword = null;
        }
    }

    public String getFormat() {
        return format;
    }

    public CastExpression setFormat(String format) {
        this.format = format;
        return this;
    }

    @Override
    public String toString() {
        String formatStr = format != null && !format.isEmpty()
                ? " FORMAT " + format
                : "";
        if (isImplicitCast) {
            return colDataType + " " + leftExpression;
        } else if (keyword != null && !keyword.isEmpty()) {
            return columnDefinitions.size() > 1
                    ? keyword + "(" + leftExpression + " AS ROW("
                            + Select.getStringList(columnDefinitions) + ")" + formatStr + ")"
                    : keyword + "(" + leftExpression + " AS " + colDataType.toString() + formatStr
                            + ")";
        } else {
            return leftExpression + "::" + colDataType.toString();
        }
    }

    public CastExpression withType(ColDataType type) {
        this.setColDataType(type);
        return this;
    }

    public CastExpression withUseCastKeyword(boolean useCastKeyword) {
        this.setUseCastKeyword(useCastKeyword);
        return this;
    }

    public CastExpression withLeftExpression(Expression leftExpression) {
        this.setLeftExpression(leftExpression);
        return this;
    }

    public <E extends Expression> E getLeftExpression(Class<E> type) {
        return type.cast(getLeftExpression());
    }

    public boolean isOf(CastExpression anotherCast) {
        return this.colDataType.equals(anotherCast.colDataType);
    }

    public boolean isOf(DataType... types) {
        return Set.of(types).contains(DataType.from(colDataType.getDataType()));
    }

    public boolean isTime() {
        return isTime(this.colDataType);
    }

    public boolean isTimeStamp() {
        return isTimeStamp(this.colDataType);
    }

    public boolean isDate() {
        return isDate(this.colDataType);
    }

    public boolean isBLOB() {
        return isBLOB(this.colDataType);
    }

    public boolean isFloat() {
        return isFloat(this.colDataType);
    }

    public boolean isInteger() {
        return isInteger(this.colDataType);
    }

    public boolean isDecimal() {
        return isDecimal(this.colDataType);
    }

    public boolean isText() {
        return isText(this.colDataType);
    }

    public enum DataType {
        ARRAY, BIT, BITSTRING, BLOB, BYTEA, BINARY, VARBINARY, BYTES, BOOLEAN, BOOL, ENUM, INTERVAL, LIST, MAP, STRUCT, TINYINT, INT1, SMALLINT, INT2, SHORT, INTEGER, INT4, INT, SIGNED, BIGINT, INT8, LONG, HUGEINT, UTINYINT, USMALLINT, UINTEGER, UBIGINT, UHUGEINT, DECIMAL, NUMBER, NUMERIC, REAL, FLOAT4, FLOAT, DOUBLE, DOUBLE_PRECISION, FLOAT8, FLOAT64, UUID, VARCHAR, NVARCHAR, CHAR, NCHAR, BPCHAR, STRING, TEXT, CLOB, DATE, TIME, TIME_WITHOUT_TIME_ZONE, TIMETZ, TIME_WITH_TIME_ZONE, TIMESTAMP_NS, TIMESTAMP, TIMESTAMP_WITHOUT_TIME_ZONE, DATETIME, TIMESTAMP_MS, TIMESTAMP_S, TIMESTAMPTZ, TIMESTAMP_WITH_TIME_ZONE, UNKNOWN, VARBYTE, JSON;

        public static DataType from(String typeStr) {
            Matcher matcher = PATTERN.matcher(typeStr.trim().replaceAll("\\s+", "_").toUpperCase());
            if (matcher.find()) {
                try {
                    return Enum.valueOf(DataType.class, matcher.group(0));
                } catch (Exception ex) {
                    Logger.getLogger(CastExpression.class.getName()).log(Level.FINE,
                            "Type " + typeStr + " unknown", ex);
                    return DataType.UNKNOWN;
                }
            } else {
                Logger.getLogger(CastExpression.class.getName()).log(Level.FINE,
                        "Type " + typeStr + " unknown");
                return DataType.UNKNOWN;
            }
        }
    }
}
