package com.lanyuanxiaoyao.service.sync.functions.type;

import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.service.common.Constants;
import com.lanyuanxiaoyao.service.common.entity.TableMeta;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.apache.flink.table.data.DecimalData;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.StringData;
import org.apache.hudi.org.apache.avro.JsonProperties;
import org.apache.hudi.org.apache.avro.LogicalTypes;
import org.apache.hudi.org.apache.avro.Schema;

import static org.apache.hudi.org.apache.avro.Schema.*;

/**
 * 类型转换第二版本
 *
 * @author ZhangJiacheng
 * @date 2023-07-20
 */
public class TypeConverterV2 implements TypeConverter {
    public static final Schema NULL_SCHEMA = create(Type.NULL);
    public static final Schema BOOLEAN_SCHEMA = create(Type.BOOLEAN);
    public static final Schema INT_SCHEMA = create(Type.INT);
    public static final Schema LONG_SCHEMA = create(Type.LONG);
    public static final Schema FLOAT_SCHEMA = create(Type.FLOAT);
    public static final Schema DOUBLE_SCHEMA = create(Type.DOUBLE);
    public static final Schema STRING_SCHEMA = create(Type.STRING);
    public static final Function<Integer, Schema> FIXED_SCHEMA = length -> createFixed("decimal_" + length, null, null, length);
    public static final BiFunction<Integer, Integer, Schema> DECIMAL_SCHEMA = (length, scala) -> LogicalTypes.decimal(length, scala).addToSchema(FIXED_SCHEMA.apply(length));
    public static final BiFunction<Integer, Integer, Schema> NULLABLE_DECIMAL_SCHEMA = (length, scala) -> createUnion(NULL_SCHEMA, DECIMAL_SCHEMA.apply(length, scala));
    public static final Schema NULLABLE_BOOLEAN_SCHEMA = createUnion(NULL_SCHEMA, BOOLEAN_SCHEMA);
    public static final Schema NULLABLE_INT_SCHEMA = createUnion(NULL_SCHEMA, INT_SCHEMA);
    public static final Schema NULLABLE_LONG_SCHEMA = createUnion(NULL_SCHEMA, LONG_SCHEMA);
    public static final Schema NULLABLE_FLOAT_SCHEMA = createUnion(NULL_SCHEMA, FLOAT_SCHEMA);
    public static final Schema NULLABLE_DOUBLE_SCHEMA = createUnion(NULL_SCHEMA, DOUBLE_SCHEMA);
    public static final Schema NULLABLE_STRING_SCHEMA = createUnion(NULL_SCHEMA, STRING_SCHEMA);
    public static final Function<Integer, Schema> NULLABLE_FIXED_SCHEMA = length -> createUnion(NULL_SCHEMA, FIXED_SCHEMA.apply(length));
    private static final Pattern BOOLEAN_REGEX = Pattern.compile("^boolean|bool$");
    private static final Pattern INT_REGEX = Pattern.compile("^(tinyint|smallint|int|smallserial|integer)(\\(\\d+\\))?$");
    private static final Pattern LONG_REGEX = Pattern.compile("^(bigint unsigned)|((bigint|serial|long)(\\(\\d+\\))?)$");
    private static final Pattern DATE_REGEX = Pattern.compile("^date|timestamp|timestamp without time zone|datetime|time$");
    private static final Pattern FLOAT_REGEX = Pattern.compile("^float(\\(\\d+\\))?$");
    private static final Pattern DOUBLE_REGEX = Pattern.compile("^double(\\(\\d+\\))?$");
    private static final Pattern FIXED_REGEX = Pattern.compile("^(number|money|bigserial)(\\(\\d+\\))?$");
    private static final Pattern DECIMAL_REGEX = Pattern.compile("^(double precision)|(decimal(\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?)$");
    private static final Pattern NUMERIC_REGEX = Pattern.compile("^numeric(\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?$");
    private static final Pattern STRING_REGEX = Pattern.compile("^(character varying|(long|medium)text)|((varchar|char|text|clob|binary|bit)(\\(\\d+\\))?)$");
    private static final Pattern YYYYMMDD = Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
    private static final DateTimeFormatter YYYYMMDD_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public Schema convertToSchema(TableMeta meta) {
        List<Field> fields = new ArrayList<>(meta.getFields().size() + 4);
        fields.add(new Field(Constants.HUDI_DELETE_KEY_NAME, BOOLEAN_SCHEMA, null, false));
        fields.add(new Field(Constants.UNION_KEY_NAME, STRING_SCHEMA, null, ""));

        for (TableMeta.FieldMeta field : meta.getFields()) {
            fields.add(new Field(field.getName(), convertType(field.getType(), field.getLength(), field.getScala()), null, JsonProperties.NULL_VALUE));
        }

        fields.add(new Field(Constants.UPDATE_TIMESTAMP_KEY_NAME, LONG_SCHEMA, null, -1));
        fields.add(new Field(Constants.LATEST_OPERATION_TIMESTAMP_KEY_NAME, STRING_SCHEMA, null, ""));
        return Schema.createRecord(meta.getTable(), null, null, false, fields);
    }

    private Schema convertType(String type, Long length, Integer scala) {
        type = type.trim().toLowerCase();
        if (BOOLEAN_REGEX.matcher(type).matches()) {
            return NULLABLE_BOOLEAN_SCHEMA;
        } else if (STRING_REGEX.matcher(type).matches() || DATE_REGEX.matcher(type).matches()) {
            return NULLABLE_STRING_SCHEMA;
        } else if (INT_REGEX.matcher(type).matches()) {
            return NULLABLE_INT_SCHEMA;
        } else if (LONG_REGEX.matcher(type).matches()) {
            return NULLABLE_LONG_SCHEMA;
        } else if (FLOAT_REGEX.matcher(type).matches()) {
            return NULLABLE_FLOAT_SCHEMA;
        } else if (DOUBLE_REGEX.matcher(type).matches()) {
            return NULLABLE_DOUBLE_SCHEMA;
        } else if (FIXED_REGEX.matcher(type).matches()) {
            return NULLABLE_DECIMAL_SCHEMA.apply(length.intValue(), 0);
        } else if (DECIMAL_REGEX.matcher(type).matches() || NUMERIC_REGEX.matcher(type).matches()) {
            if (ObjectUtil.isNull(scala)) {
                return NULLABLE_DECIMAL_SCHEMA.apply(length.intValue(), 6);
            } else {
                return NULLABLE_DECIMAL_SCHEMA.apply(length.intValue(), scala);
            }
        } else {
            throw new RuntimeException(Constants.LOG_POINT_FIELD_TYPE_NOT_FOUND + " Cannot find correct type for source type: " + type + " length: " + length);
        }
    }

    @Override
    public GenericRowData convertToGenericRowData(TableMeta meta, Schema schema, Map<String, Object> data) {
        List<Field> fields = schema.getFields();
        GenericRowData row = new GenericRowData(fields.size());
        for (int index = 0; index < fields.size(); index++) {
            Field field = fields.get(index);
            Object value = data.getOrDefault(Constants.FIELD_COVERT.apply(meta, field.name()), null);
            row.setField(index, covertValue(field.schema(), value));
        }
        return row;
    }

    private Object covertValue(Schema schema, Object value) {
        if (ObjectUtil.isNull(value)) {
            return value;
        } else if (NULLABLE_BOOLEAN_SCHEMA.equals(schema) || BOOLEAN_SCHEMA.equals(schema)) {
            return value instanceof String ? Boolean.valueOf((String) value) : value;
        } else if (NULLABLE_INT_SCHEMA.equals(schema) || INT_SCHEMA.equals(schema)) {
            return value instanceof String ? Integer.valueOf((String) value) : value;
        } else if (NULLABLE_LONG_SCHEMA.equals(schema) || LONG_SCHEMA.equals(schema)) {
            return value instanceof String ? Long.valueOf((String) value) : value;
        } else if (NULLABLE_FLOAT_SCHEMA.equals(schema) || FLOAT_SCHEMA.equals(schema)) {
            return value instanceof String ? Float.valueOf((String) value) : value;
        } else if (NULLABLE_DOUBLE_SCHEMA.equals(schema) || DOUBLE_SCHEMA.equals(schema)) {
            return value instanceof String ? Double.valueOf((String) value) : value;
        } else if (NULLABLE_STRING_SCHEMA.equals(schema) || STRING_SCHEMA.equals(schema)) {
            return StringData.fromString((String) value);
        } else {
            for (Schema type : schema.getTypes()) {
                if (type.getLogicalType() instanceof LogicalTypes.Decimal) {
                    LogicalTypes.Decimal decimalType = (LogicalTypes.Decimal) type.getLogicalType();
                    int precision = decimalType.getPrecision();
                    int scala = decimalType.getScale();
                    return DecimalData.fromBigDecimal(new BigDecimal((String) value), precision, scala);
                }
            }
            return value;
        }
    }
}
